El teléfono de guardia vibra. La API devuelve 500s. La latencia está bien, la CPU está aburrida, la memoria está satisfecha.
Entonces lo ves: No space left on device. No en el host que dimensionaste con cuidado—sino dentro del sistema de archivos del contenedor,
en la pila de capas, en los logs, en el directorio “temporal” que se suponía que sería temporal en 2019.
Esta es la interrupción más común y aburrida en el mundo de los contenedores. También es completamente prevenible si dejas de tratar al disco como
un pozo sin fondo y empiezas a tratar /tmp, los logs y las cachés como cosas con presupuestos.
El modelo mental: dónde vive realmente el “disco del contenedor”
Cuando alguien dice “el sistema de archivos del contenedor está lleno”, lo que suele querer decir es: una capa escribible asociada a un contenedor,
almacenada en el host bajo el controlador de almacenamiento de Docker (comúnmente overlay2), se quedó sin espacio en el sistema de archivos subyacente del host.
Ese es el comportamiento por defecto para los cambios del sistema de archivos raíz de un contenedor. No es magia. Es un árbol de directorios y unos trucos de mount.
Los contenedores no son máquinas virtuales. No traen su propio dispositivo de bloques a menos que se lo des correspondiente mediante volúmenes, mounts o CSI.
Si no fijas presupuestos de almacenamiento, un contenedor puede comer felizmente el disco del host. Es educado de esa manera.
Hechos interesantes y contexto histórico (porque las interrupciones tienen ancestros)
- Los sistemas de ficheros en unión preceden a Docker: el apilamiento estilo overlay viene del largo intento del mundo Linux por hacer bases inmutables con tops escribibles prácticas.
- Los controladores de almacenamiento iniciales de Docker eran un zoológico: aufs, devicemapper, btrfs, overlay, overlay2—cada uno con modos de fallo y comportamientos de limpieza distintos.
- El logging json-file se volvió la “trampa por defecto”: era simple y funcionaba en todas partes, así que silenciosamente se convirtió en la fuga de disco de muchos.
- La rotación de logs es anterior a los contenedores: syslog y logrotate existen porque llenar discos es un rito clásico de Unix.
- Las cachés de build explotaron con el CI moderno: una vez que los multi-stage builds y el cacheo de capas se popularizaron, la caché de build se volvió residente en producción.
- Los runtimes no inventaron el almacenamiento efímero: tmpfs lleva existiendo siempre; simplemente seguimos olvidando usarlo para datos realmente temporales.
- Kubernetes elevó “almacenamiento efímero” a recurso agendable: principalmente porque suficientes nodos murieron por crecimiento de logs y emptyDir.
- Las semánticas de OverlayFS importan: borrar un archivo dentro de un contenedor no siempre “libera espacio” si algo todavía mantiene un handle abierto (clásico problema con archivos de log).
Una cita que vale la pena pegar en tu monitor:
La esperanza no es una estrategia.
— comúnmente atribuida en círculos de operaciones; idea parafraseada de prácticas de fiabilidad.
La gestión de disco no es lugar para las vibras.
Broma #1: El disco es el único recurso que empieza infinito y luego se vuelve abruptamente cero a las 03:00.
Guion de diagnóstico rápido
Esta es la secuencia que uso cuando alguien avisa “contenedor con disco lleno” y espera que sea un mago.
No es magia. Es una lista de verificación con opiniones firmes.
Primero: confirma qué está realmente lleno
- ¿Sistema de archivos del host? Si la partición del host que sostiene Docker está llena, todo está en llamas, incluso si solo un contenedor hace ruido.
- ¿Área de almacenamiento de Docker?
/var/lib/docker(o tudata-rootconfigurado) a menudo está en la partición equivocada. - ¿Un volumen? Los volúmenes pueden llenarse independientemente y no aparecerán como uso de “capa del contenedor”.
- ¿Agotamiento de inodos? Puedes tener muchos bytes libres y cero inodos. Los archivos pequeños son una gran forma de arruinar tu fin de semana.
Segundo: encuentra la categoría de crecimiento
- Logs (logs de Docker, logs de la app, access logs, volcados de depuración).
- Tmp (subidas, archivos extraídos, archivos temporales de OCR, espacio de scratch para procesamiento de imágenes).
- Cachés (cachés de paquetes, cachés de dependencias, cachés de runtimes, cachés de navegador).
- Artefactos de build (restos de CI, caché de build, capas).
- Datos escritos por accidente en la capa escribible del contenedor en lugar de un volumen.
Tercero: elige la válvula de alivio de menor riesgo
- Rotar / limitar logs (preferiblemente sin reiniciar).
- Eliminar directorios temporales seguros (con reglas claras de propiedad y antigüedad).
- Prune de artefactos de Docker (con objetivo cuidadoso).
- Mover la ruta caliente a un volumen o tmpfs (la solución real).
El cuello de botella no es el “disco”. El cuello de botella es la ausencia de un presupuesto y de un responsable.
Tu objetivo: identificar al responsable y aplicar el presupuesto.
Tareas prácticas: comandos, salidas y decisiones (12+)
Estas son tareas reales que hago bajo presión. Cada una incluye: comando, lo que significa la salida y qué decisión tomar a continuación.
Ejecútalas en el host salvo que se indique lo contrario.
Tarea 1: ¿El sistema de archivos del host está realmente lleno?
cr0x@server:~$ df -hT
Filesystem Type Size Used Avail Use% Mounted on
/dev/sda2 ext4 100G 96G 2.0G 98% /
tmpfs tmpfs 3.1G 4.0M 3.1G 1% /run
/dev/sdb1 ext4 500G 120G 355G 26% /data
Significado: / está al 98% de uso. Si Docker’s data-root está en /, estás cerca de una parada completa.
Decisión: Confirma dónde Docker almacena datos. Si está en /, prioriza limpieza inmediata y planifica una migración a /data.
Tarea 2: ¿Dónde guarda Docker sus datos?
cr0x@server:~$ docker info --format 'DockerRootDir={{.DockerRootDir}} Driver={{.Driver}} LoggingDriver={{.LoggingDriver}}'
DockerRootDir=/var/lib/docker Driver=overlay2 LoggingDriver=json-file
Significado: La raíz de Docker está en /var/lib/docker (en /) usando overlay2. Los logs son json-file.
Decisión: Espera directorios overlay2 y logs json como sospechosos principales. No “prunes todo” todavía.
Tarea 3: Vista rápida del desglose de uso de disco de Docker
cr0x@server:~$ docker system df -v
TYPE TOTAL ACTIVE SIZE RECLAIMABLE
Images 24 12 18.4GB 6.2GB (33%)
Containers 36 18 4.8GB 1.1GB (22%)
Local Volumes 19 15 220.5GB 12.0GB (5%)
Build Cache 112 0 41.7GB 41.7GB
Significado: Los volúmenes dominan (220GB). La caché de build es grande y totalmente recuperable. Los contenedores suman ~5GB.
Decisión: Si necesitas alivio rápido, la caché de build es un objetivo de bajo riesgo. Pero audita también los volúmenes—alguien está almacenando datos reales.
Tarea 4: Encuentra los directorios más grandes bajo /var/lib/docker
cr0x@server:~$ sudo du -xhd1 /var/lib/docker | sort -h
12G /var/lib/docker/containers
41G /var/lib/docker/buildkit
67G /var/lib/docker/overlay2
220G /var/lib/docker/volumes
340G /var/lib/docker
Significado: Volúmenes y overlay2 son pesados; buildkit es sustancial.
Decisión: Identifica qué contenedores mapean a esos volúmenes. No borres datos de volúmenes a menos que sepas el propietario de la aplicación y la política de retención.
Tarea 5: ¿Qué contenedores generan logs Docker enormes?
cr0x@server:~$ sudo find /var/lib/docker/containers -name '*-json.log' -printf '%s %p\n' | sort -n | tail -5
187654321 /var/lib/docker/containers/4c9f.../4c9f...-json.log
322198765 /var/lib/docker/containers/8a21.../8a21...-json.log
988877766 /var/lib/docker/containers/1b77.../1b77...-json.log
2147483648 /var/lib/docker/containers/aa11.../aa11...-json.log
4123456789 /var/lib/docker/containers/f00d.../f00d...-json.log
Significado: Tienes logs json de varios GB. Eso es presión de disco con nombre y dirección.
Decisión: Identifica los nombres de los contenedores para esos IDs, luego limita/rota los logs. No borres los archivos a lo loco si el daemon sigue escribiendo.
Tarea 6: Mapea un ID de contenedor a un nombre e imagen
cr0x@server:~$ docker ps --no-trunc --format '{{.ID}} {{.Names}} {{.Image}}' | grep f00d
f00dbeefcafe1234567890abcdef1234567890abcdef1234567890abcdef api-prod myorg/api:2026.01
Significado: El contenedor API está generando logs enormes.
Decisión: Trata los logs como síntoma. Aún necesitas corregir el nivel de logging ruidoso o el bucle de errores.
Tarea 7: Comprueba el tamaño de la capa escribible del contenedor (prueba rápida)
cr0x@server:~$ docker ps -q | head -5 | xargs -I{} docker container inspect --format '{{.Name}} rw={{.SizeRw}} rootfs={{.SizeRootFs}}' {}
/api-prod rw=2147483648 rootfs=0
/worker-prod rw=536870912 rootfs=0
/nginx-prod rw=104857600 rootfs=0
/cron-prod rw=0 rootfs=0
/metrics rw=0 rootfs=0
Significado: SizeRw (capa escribible) para api-prod es ~2GB. Eso suele ser logs, tmp o escrituras accidentales de datos.
Decisión: Haz exec en ese contenedor para encontrar dónde vive el espacio, o revisa el mapeo de directorios diff en overlay2.
Tarea 8: Dentro del contenedor, encuentra los mayores consumidores de disco
cr0x@server:~$ docker exec -it api-prod sh -lc 'df -h; du -xhd1 / | sort -h | tail -10'
Filesystem Size Used Available Use% Mounted on
overlay 100G 98G 2.0G 98% /
tmpfs 64M 0 64M 0% /dev
tmpfs 3.1G 0 3.1G 0% /sys/fs/cgroup
1.2G /var
1.5G /usr
2.8G /tmp
6.0G /app
9.1G /log
Significado: /tmp y /log son grandes. Eso es accionable.
Decisión: Determina si esos directorios deberían ser tmpfs, respaldados por volumen o limpiados agresivamente.
Tarea 9: Encuentra archivos individuales grandes (a menudo core dumps, volcados o subidas atascadas)
cr0x@server:~$ docker exec -it api-prod sh -lc 'find /tmp /log -type f -size +200M -printf "%s %p\n" | sort -n | tail -10'
2147483648 /log/app.log
536870912 /tmp/upload-20260103-0130.tmp
402653184 /tmp/image-cache.bin
Significado: Un solo log tiene 2GB; un archivo temporal de subida pesa 512MB. Esto no es sutil.
Decisión: Limita los logs (rota). Para subidas temporales, asegúrate de limpiar en éxito/fallo y considera tmpfs o un volumen dedicado con cuotas.
Tarea 10: Comprueba si “archivos borrados” siguen ocupando espacio por manejadores abiertos
cr0x@server:~$ sudo lsof +L1 | grep '/var/lib/docker/overlay2' | head
node 24781 root 21w REG 8,2 2147483648 0 /var/lib/docker/overlay2/7a2.../diff/log/app.log (deleted)
Significado: El archivo fue borrado pero sigue abierto. El espacio no se liberará hasta que el proceso reinicie o cierre el descriptor de archivo.
Decisión: Reinicia el contenedor (o recarga el proceso) después de asegurarte de que volverá en estado limpio. También arregla la rotación de logs para que esto no se repita.
Tarea 11: Prune de caché de build de forma segura (usualmente bajo riesgo)
cr0x@server:~$ docker builder prune --filter 'until=168h'
WARNING! This will remove all dangling build cache.
Are you sure you want to continue? [y/N] y
Deleted build cache objects:
r1m2n3...
freed: 38.6GB
Significado: Recuperaste ~39GB sin tocar contenedores en ejecución.
Decisión: Si esto fue el mayor consumidor, programa este prune en runners CI o nodos de construcción con una política de retención.
Tarea 12: Prune de imágenes no usadas (con los ojos abiertos)
cr0x@server:~$ docker image prune -a
WARNING! This will remove all images without at least one container associated to them.
Are you sure you want to continue? [y/N] y
Deleted Images:
deleted: sha256:1a2b...
Total reclaimed space: 5.9GB
Significado: Imágenes antiguas ocupaban espacio; recuperaste ~6GB.
Decisión: En nodos de producción, podar imágenes puede ralentizar redeploys y romper rollbacks si no tienes cuidado. Prefiere ejecutar esto en CI o nodos con estrategia de despliegue controlada.
Tarea 13: Identifica volúmenes grandes y mapea a contenedores
cr0x@server:~$ docker volume ls -q | xargs -I{} sh -lc 'p=$(docker volume inspect -f "{{.Mountpoint}}" {}); s=$(sudo du -sh "$p" 2>/dev/null | awk "{print \$1}"); echo "$s {} $p"' | sort -h | tail -5
18G pgdata /var/lib/docker/volumes/pgdata/_data
22G redisdata /var/lib/docker/volumes/redisdata/_data
41G uploads /var/lib/docker/volumes/uploads/_data
55G elastic /var/lib/docker/volumes/elastic/_data
62G app-cache /var/lib/docker/volumes/app-cache/_data
Significado: Hay un volumen “app-cache” de 62GB. Eso puede ser legítimo, o un crecimiento de caché sin límites.
Decisión: Inspecciona qué contenedores lo montan, luego establece políticas de expulsión/retención. Cache no es dato; trátalo como descartable con límites.
Tarea 14: ¿Qué contenedores montan ese volumen?
cr0x@server:~$ docker ps --format '{{.Names}}' | xargs -I{} sh -lc 'docker inspect -f "{{.Name}} {{range .Mounts}}{{.Name}}:{{.Destination}} {{end}}" {}' | grep app-cache
/api-prod app-cache:/app/cache
/worker-prod app-cache:/app/cache
Significado: Dos servicios comparten el volumen de caché. Las cachés compartidas son cómodas y también excelentes para convertirse en vertederos.
Decisión: Implementa límites de tamaño en la aplicación y programa limpieza. Si la caché es reconstruible, considera cachés por pod o tmpfs.
Tarea 15: Comprueba el agotamiento de inodos (la versión “muchos archivos pequeños” de disco lleno)
cr0x@server:~$ df -hi
Filesystem Inodes IUsed IFree IUse% Mounted on
/dev/sda2 6.4M 6.4M 12K 100% /
Significado: Te quedaste sin inodos, no con bytes. Causas comunes: archivos de logs sin límite, directorios de caché con millones de entradas o tormentas de archivos temporales.
Decisión: Identifica el directorio con más archivos y límpialo. Considera tunear el sistema de ficheros o mover DockerRootDir a un sistema con más inodos.
Tarea 16: Encuentra directorios con recuentos de archivos locos
cr0x@server:~$ sudo sh -lc 'for d in /var/lib/docker /var/log /tmp; do echo "## $d"; find "$d" -xdev -type f 2>/dev/null | wc -l; done'
## /var/lib/docker
1892345
## /var/log
45678
## /tmp
1203
Significado: La mayoría de los archivos están bajo el área de Docker. Overlay2 y los logs de contenedores pueden explotar el uso de inodos.
Decisión: Afina dentro de /var/lib/docker, empezando por overlay2 y containers. Si es caché de build, púlela.
/tmp y archivos temporales: estrategias de limpieza que no borran lo equivocado
El espacio temporal es donde las buenas intenciones fermentan. En contenedores, /tmp suele estar en la capa escribible, lo que significa:
compite con todo lo demás por el mismo disco del host y sobrevive más tiempo del que crees (los reinicios del contenedor no necesariamente lo limpian).
Estrategia: decide qué significa “temporal” para tu carga de trabajo
- Scratch realmente efímero (procesado de imágenes, descompresión, ordenar, buffering pequeño): usa
tmpfscon un tope de tamaño. - Temp grande para subidas (multi-GB): usa un volumen dedicado o una ruta del host con cuotas; no finjas que tmpfs te salvará.
- Colas de trabajo y archivos de spill: hazlos explícitos y observables; fija TTL y tamaño máximo.
Montar /tmp como tmpfs (buen predeterminado para muchas apps web)
En Docker run:
cr0x@server:~$ docker run --rm -it --tmpfs /tmp:rw,noexec,nosuid,size=256m alpine sh -lc 'df -h /tmp'
Filesystem Size Used Available Use% Mounted on
tmpfs 256.0M 0 256.0M 0% /tmp
Significado: /tmp ahora está en RAM y limitado a 256MB.
Decisión: Si tu app usa /tmp para archivos de scratch pequeños, esto evita fugas de disco y hace los fallos ruidosos (comportamiento tipo ENOMEM) en lugar de rellenar silenciosamente el host.
Para Compose:
cr0x@server:~$ cat docker-compose.yml
services:
api:
image: myorg/api:2026.01
tmpfs:
- /tmp:rw,noexec,nosuid,size=256m
Regla de limpieza: no hagas “rm -rf /tmp” en producción como un villano de dibujos
El enfoque seguro es la eliminación basada en antigüedad en un directorio que tu app posea, no una eliminación global.
Haz que tu app escriba en /tmp/myapp, y limpia solo ese subárbol.
cr0x@server:~$ docker exec -it api-prod sh -lc 'mkdir -p /tmp/myapp; find /tmp/myapp -type f -mmin +120 -delete; echo "cleanup done"; du -sh /tmp/myapp'
cleanup done
12M /tmp/myapp
Significado: Se eliminaron archivos de más de dos horas; el directorio ahora tiene 12MB.
Decisión: Si esto ayuda repetidamente, tu app está filtrando archivos temporales en rutas de error. Arregla el código, pero mantén la escoba.
Broma #2: Nada es más permanente que un directorio temporal sin propietario.
Logs: json-file de Docker, journald y logs de la aplicación
Los logs son la razón número 1 por la que “contenedores llenaron el disco” aparece en cronogramas de incidentes.
No porque el logging sea malo—sino porque registrar sin límites es un ataque DoS a cámara lenta contra tu propio disco.
json-file de Docker: pon límites o te volcará
El logging por defecto de Docker (json-file) escribe logs por contenedor bajo /var/lib/docker/containers/<id>/<id>-json.log.
Si no configuras rotación, crecen indefinidamente. Indefinidamente es más largo que tu disco.
Configura rotación de logs en /etc/docker/daemon.json
cr0x@server:~$ sudo cat /etc/docker/daemon.json
{
"log-driver": "json-file",
"log-opts": {
"max-size": "50m",
"max-file": "5"
}
}
Significado: El log gestionado por Docker de cada contenedor está limitado a ~250MB (5 archivos × 50MB).
Decisión: Aplica esto en todos los hosts. Luego reinicia Docker en una ventana de mantenimiento. Si no puedes reiniciar, planifica un despliegue gradual; no lo ignores.
¿Y journald?
Si Docker escribe en journald, el crecimiento se traslada al journal del sistema. Buena noticia: journald soporta límites de tamaño centralizados.
Mala noticia: tu disco sigue llenándose, solo con archivos de otra forma.
cr0x@server:~$ sudo journalctl --disk-usage
Archived and active journals take up 7.8G in the file system.
Significado: Los journals ocupan 7.8GB. No es catastrófico, pero no es gratis.
Decisión: Fija retención (SystemMaxUse, SystemMaxFileSize) si esto tiende a crecer.
Logs de la app dentro del contenedor: decide dónde pertenecen
Generalmente quieres que las apps registren a stdout/stderr y que la plataforma maneje el envío de logs. Cuando las apps escriben en /log
dentro del sistema de archivos del contenedor, has creado un segundo sistema de logs con reglas de retención distintas y mayor tendencia a llenar discos silenciosamente.
Si debes escribir archivos (cumplimiento, procesos batch, apps legacy), ponlos en un volumen con retención y rotación explícitas.
Y prueba la rotación: rotar logs es un cambio que puede romper cosas.
Trunca con seguridad cuando el archivo sea demasiado grande (solo emergencia)
cr0x@server:~$ sudo sh -lc ': > /var/lib/docker/containers/f00dbeefcafe*/f00dbeefcafe*-json.log; ls -lh /var/lib/docker/containers/f00dbeefcafe*/f00dbeefcafe*-json.log'
-rw-r----- 1 root root 0 Jan 3 02:11 /var/lib/docker/containers/f00dbeefcafe123.../f00dbeefcafe123...-json.log
Significado: Truncaste el archivo en el lugar. Docker puede seguir escribiendo sin tener que reabrir un nuevo inode.
Decisión: Haz esto solo como medida temporal durante un incidente. Tu solución real es rotación de logs + reducir el volumen de logs en la fuente.
Cachés: gestores de paquetes, runtimes y artefactos de compilación
La caché es una característica de rendimiento que se convierte en bug de almacenamiento cuando nadie la posee. Los contenedores amplifican esto porque los mismos patrones de caché
que están bien en un portátil de desarrollo se transforman en crecimiento sin límites en un nodo con docenas de servicios.
Puntos calientes comunes de caché
- Gestores de paquetes del SO: cachés de apt, apk, yum dejadas en imágenes o creadas en tiempo de ejecución.
- Runtimes: caché de pip, caché de npm, Maven/Gradle, gemas de Ruby, caché de compilación de Go.
- Cachés de navegadores sin cabeza: los directorios de caché de Chromium pueden crecer desmesuradamente.
- Cachés a nivel de aplicación: cachés de miniaturas, plantillas compiladas, cachés de consultas, exportaciones “temporales”.
Dentro de un contenedor en ejecución: encuentra directorios de caché
cr0x@server:~$ docker exec -it worker-prod sh -lc 'du -xhd1 /root /var/cache /app | sort -h | tail -15'
12M /var/cache
420M /root
2.3G /app
Significado: Algo bajo /app es grande; el home de root pesa 420MB (a menudo cachés de runtime).
Decisión: Inspecciona el subárbol /app en busca de directorios de caché y decide: ¿debe ser un volumen, tener un tope de tamaño, o borrarse al arrancar?
Evita que las cachés terminen en la capa escribible
Si la caché es prescindible y local, monta un volumen dedicado a la caché. Así no hincha la capa del contenedor y es más fácil de limpiar.
Si necesitas límites duros, considera cuotas de sistema de ficheros en esa ruta de volumen a nivel de host (o usa clases de almacenamiento en Kubernetes).
Además: no construyas imágenes que lleven cachés. Si usas Dockerfiles, limpia las cachés de paquetes en la misma capa en la que instalas paquetes,
de lo contrario la capa todavía contendrá los datos antiguos. No es juicio moral; así funcionan los sistemas de archivos en capas.
Artefactos de Docker: imágenes, capas, overlay2, caché de build
Docker crea y retiene muchas cosas: imágenes, capas, contenedores detenidos, redes, volúmenes, caché de build.
Las reglas de retención son intencionalmente conservadoras porque borrar lo equivocado rompe flujos de trabajo. Tu trabajo es afinar esas reglas para tu entorno.
Crecimiento de overlay2: el coste oculto de “escribir en root”
Overlay2 almacena los cambios escribibles por contenedor en directorios bajo /var/lib/docker/overlay2.
Cuando una aplicación escribe en /var/lib/postgresql o /uploads sin un volumen, escribe en overlay2.
Funciona hasta que el nodo muere de éxito.
Encuentra qué contenedores tienen capas escribibles grandes
cr0x@server:~$ docker ps -q | xargs -I{} docker inspect --format '{{printf "%-25s %-12s\n" .Name .SizeRw}}' {} | sort -k2 -n | tail
/api-prod 2147483648
/worker-prod 536870912
/nginx-prod 104857600
Significado: Estos contenedores están escribiendo datos significativos en la capa escribible.
Decisión: Para cada uno, decide si las escrituras deberían ir a stdout, tmpfs o a un volumen con nombre. “Mantenerlo en el contenedor” no es una estrategia de almacenamiento.
Poda (pruning): útil, peligroso y a veces ambas cosas
docker system prune es la motosierra. Es útil cuando el bosque está en llamas, pero también puedes amputar tu capacidad de hacer rollback o reconstruir rápido.
En producción, prefiere prunes dirigidos:
- Prune de caché de build en nodos de build y runners CI.
- Prune de imágenes con conciencia de la estrategia de despliegue y necesidades de rollback.
- Prune de contenedores solo si acumulas contenedores detenidos sin motivo.
- Prune de volúmenes solo cuando tengas garantías fuertes de que no borrarás datos en uso.
cr0x@server:~$ docker container prune
WARNING! This will remove all stopped containers.
Are you sure you want to continue? [y/N] y
Deleted Containers:
3d2c...
Total reclaimed space: 1.0GB
Significado: Los contenedores detenidos consumían espacio.
Decisión: Si regularmente ves muchos contenedores detenidos, arregla tu proceso de despliegue o tus crash loops. La limpieza no es la solución raíz.
Volúmenes y bind mounts: el disco “que no está realmente en el contenedor”
Los volúmenes son donde deben vivir los datos duraderos. También son donde las cachés van a convertirse en “duraderas por accidente.”
El modo de fallo es predecible: la aplicación crece silenciosamente un directorio, el disco del nodo se llena y todos culpan a Docker.
Audita volúmenes como auditas bases de datos
El uso de volúmenes es operacionalmente significativo. Pon alertas en ello. Pon responsables en ello. Si es una caché, aplica expulsión y tamaño máximo.
Si son datos duraderos, aplica backups y políticas de retención.
Bind mounts: convenientes y afilados
Los bind mounts pueden saltarse el almacenamiento gestionado por Docker y escribir directamente en el sistema de archivos del host. Eso puede ser bueno (separar datos de DockerRootDir)
o terrible (escribir en / en una partición root pequeña).
cr0x@server:~$ docker inspect api-prod --format '{{range .Mounts}}{{.Type}} {{.Source}} -> {{.Destination}}{{"\n"}}{{end}}'
volume /var/lib/docker/volumes/app-cache/_data -> /app/cache
bind /data/uploads -> /uploads
Significado: Uploads está bind-montado a /data/uploads. Genial—si /data es grande y está monitorizado.
Decisión: Confirma que la ruta del host tiene cuotas/alertas. Los bind mounts son efectivamente “escribir al host”, así que trátalos como tal.
Notas de Kubernetes: almacenamiento efímero y DiskPressure
En Kubernetes, los problemas de disco suelen presentarse como DiskPressure, evicciones de pods y nodos quedando NotReady.
Las causas subyacentes son las mismas: logs, emptyDir, capas escribibles de contenedor, caché de imágenes y volúmenes.
Kubernetes solo añade un planificador y un modo de fallo más ruidoso.
Punto operativo clave: solicita y limita almacenamiento efímero
Si no estableces requests/limits para almacenamiento efímero, el kubelet no puede tomar decisiones inteligentes de scheduling y recurrirá a evictar pods cuando el nodo ya esté sufriendo.
Eso no es planificación de capacidad; eso es gestión de pánico.
emptyDir no es un cubo de basura sin tapa
emptyDir sin sizeLimit (y sin monitorización) es un cheque en blanco.
Si la carga necesita espacio de scratch, fija un presupuesto. Si necesita almacenamiento duradero, usa un PVC.
cr0x@server:~$ kubectl describe node worker-3 | sed -n '/Conditions:/,/Addresses:/p'
Conditions:
Type Status LastHeartbeatTime Reason Message
DiskPressure True Fri, 03 Jan 2026 02:19:54 +0000 KubeletHasDiskPressure kubelet has disk pressure
Significado: El nodo está bajo presión de disco y empezará a evictar.
Decisión: Identifica inmediatamente los principales consumidores (logs de contenedores, imágenes, emptyDir, capas escribibles) y alivia la presión. Luego añade límites y alertas.
Tres mini-historias corporativas (realistas, dolorosas, útiles)
1) Incidente causado por una suposición errónea: “Los contenedores son efímeros, así que no pueden llenar disco”
Una empresa mediana ejecutaba una API relacionada con pagos en un puñado de hosts Docker. La app era stateless, así que todos asumieron que el almacenamiento “no era tema”.
Los despliegues eran algo blue/green: nuevos contenedores surgían, los viejos se detenían, y nadie prestó atención a los detenidos porque “no están en ejecución”.
Durante unos meses, el filesystem root de los hosts subió silenciosamente de cómodo a precario. El culpable real fue aburrido:
logs json de Docker en unos cuantos contenedores muy verbosos, además de docenas de contenedores detenidos retenidos para “debugging más tarde”.
Los logs nunca se rotaron. Los contenedores detenidos tenían capas escribibles llenas de archivos temporales creados durante picos de requests.
El corte ocurrió durante un deploy rutinario. Docker intentó tirar una nueva capa de imagen. El pull falló con no space left on device.
La lógica de orquestación siguió intentando, creando más descargas parciales y más logs de error. Ahora el disco no estaba solo lleno; estaba siendo atacado activamente por reintentos.
La recuperación fue la comedia favorita de un comandante de incidentes: la solución más rápida fue detener el bucle de reintentos y podar la caché de build y las imágenes viejas.
Pero el equipo intentó inicialmente borrar archivos de log dentro de contenedores, sin darse cuenta de que Docker escribía en otro lugar. Recuperaron casi nada y perdieron tiempo.
Una vez rotaron los logs de Docker y prunearon contenedores detenidos, el nodo se recuperó.
La causa raíz no fue “Docker”. Fue la suposición de que el cómputo efímero implica almacenamiento efímero.
Los contenedores son efímeros; los archivos que crean en el host son muy comprometidos con la relación.
2) Optimización que salió mal: “Cacheemos todo en un volumen compartido”
Otra organización tenía requests costosos: generación de PDFs y renderizado de imágenes. Alguien sugirió sensatamente:
cachear activos intermedios y salidas renderizadas para que las requests repetidas sean más rápidas. Lo implementaron como un volumen Docker compartido en /app/cache
por la API y los contenedores worker.
El rendimiento mejoró de inmediato. La latencia bajó, la CPU también, y el equipo se felicitó por “escalar sin escalar”.
El directorio de caché se volvió una historia de éxito en la revisión semanal de métricas. Nadie preguntó qué tan grande podía llegar a ser o cómo se limpiaría.
Un mes después, el nodo empezó a flaquear. No CPU, no memoria—disco. El volumen de caché había crecido de forma sostenida y luego abrupta,
porque una nueva característica aumentó la variedad de claves de caché y redujo la tasa de hits. La caché dejó de comportarse como caché y empezó a comportarse como una segunda base de datos,
pero sin compactación, sin TTL y sin backups.
El modo de fallo fue sigiloso: una vez lleno el volumen, las escrituras fallaron. La app interpretó los fallos de escritura de caché como “miss”,
así que recomputó el trabajo costoso. Eso aumentó la carga. El sistema se ralentizó, aumentaron los errores y la caché siguió thrasheando.
Fue una optimización de rendimiento que se transformó en un bug de fiabilidad.
La solución no fue heroica: imponer un tamaño máximo de caché, expulsar por LRU/edad y tratar el volumen de caché como un recurso con presupuesto.
También hicieron la caché por-worker en algunos casos para evitar un punto caliente compartido. El rendimiento se mantuvo y el disco dejó de ser el asesino silencioso.
3) Práctica aburrida pero correcta que salvó el día: topes rígidos de logs y revisión semanal del presupuesto de artefactos
Un gran equipo de plataforma interna ejecutaba cientos de contenedores por clúster. Eran alérgicos a incidentes de disco porque son ruidosos, políticos
y suelen ocurrir cuando la dirección está viendo una demo.
Su enfoque fue agresivamente aburrido. Cada nodo tenía rotación de logs de Docker configurada por defecto. Cada carga tenía una política documentada:
si registras a stdout, obtienes envío centralizado; si registras a archivos, debes rotarlos y almacenarlos en un volumen dedicado.
Tenían una revisión semanal del “presupuesto de artefactos”: volúmenes top, imágenes top, cachés de build top y productores de logs top. No era una sesión de culpables—solo una limpieza de higiene.
Durante un incidente, un servicio entró en un bucle de errores y comenzó a emitir trazas de pila verbosas. En otro entorno, eso habría llenado discos
y derribado servicios no relacionados. Aquí, el tope de logs de Docker mantuvo el daño contenido. La observabilidad no desapareció: los logs siguieron fluyendo,
solo con rotación y retención acotada a nivel de nodo.
El equipo aún tuvo que arreglar el bug. Pero no tuvieron que recuperar una flota de la agotamiento de disco mientras la arreglaban.
Ese es el beneficio real de las guardas aburridas: te compran tiempo para hacer el trabajo real.
Errores comunes (síntoma → causa raíz → solución)
1) “El disco está lleno pero borré archivos y no cambió nada”
Síntoma: Eliminas archivos de logs grandes, pero df aún muestra poco espacio libre.
Causa raíz: El proceso todavía tiene el archivo abierto (inode borrado pero abierto). Clásico con archivos de log.
Solución: Usa lsof +L1 para encontrar archivos eliminados abiertos, luego reinicia el contenedor/proceso o rota logs correctamente.
2) “Hice docker system prune y el disco se llenó otra vez al día siguiente”
Síntoma: Alivio temporal seguido de agotamiento recurrente de disco.
Causa raíz: Trataste síntomas (artefactos) mientras la carga sigue generando logs/tmp/cachés sin control.
Solución: Añade límites de rotación de logs; monta tmp como tmpfs con tamaño; implementa expulsión de caché; mueve escrituras de datos a volúmenes con retención.
3) “El contenedor dice disco lleno pero el host tiene espacio”
Síntoma: El host /data tiene cientos de GB libres, pero el overlay del contenedor muestra 98% usado.
Causa raíz: DockerRootDir está en / (pequeño), mientras otras particiones son grandes.
Solución: Mueve Docker’s data-root al sistema de archivos más grande (migración planificada). A corto plazo: prune de caché de build/imágenes y cap de logs.
4) “Configuramos rotación de logs pero los logs siguen explotando”
Síntoma: Los logs json siguen creciendo más allá de lo esperado.
Causa raíz: La configuración de rotación se aplicó solo a contenedores nuevos; o se usa otro driver de logging; o la app escribe logs a archivos en lugar de stdout.
Solución: Verifica docker info driver de logging y opciones por contenedor. Redeploy de contenedores tras cambios en la configuración del daemon.
5) “Los inodos llegaron al 100% con muchos GB libres”
Síntoma: df -hi muestra 100% de uso de inodos.
Causa raíz: Millones de archivos pequeños: directorios de caché, archivos extraídos, creación descontrolada de archivos temporales.
Solución: Identifica hotspots por recuento de archivos; limpia agresivamente; rediseña almacenamiento de caché (menos ficheros) o usa un filesystem con más inodos.
6) “El volumen sigue creciendo pero es ‘solo caché’”
Síntoma: Un volumen de caché crece sin límites.
Causa raíz: La caché no tiene TTL/expulsión; las claves se vuelven efectivamente únicas; cambió la carga de trabajo.
Solución: Implementa expulsión y tamaño máximo en la lógica de la app; considera caches por instancia; añade alertas y dashboards para volúmenes.
7) “El job de limpieza borró los archivos incorrectos”
Síntoma: La app falla después de un cron de limpieza.
Causa raíz: La limpieza ejecutó borrados amplios (p. ej., /tmp o /var) sin rutas propias de la app y reglas de antigüedad.
Solución: Restringe la limpieza a rutas poseídas por la app; usa eliminación basada en antigüedad; escribe en directorios dedicados; prueba en staging con cargas realistas.
Listas de verificación / plan paso a paso
Paso a paso: alivio de emergencia (mantener servicios vivos)
- Confirma qué está lleno:
df -hTydf -hi. Decide si son bytes o inodos. - Revisa el desglose de Docker:
docker system df -vpara ver si son volúmenes, caché de build, imágenes o contenedores. - Detén la hemorragia: si los logs son enormes, trúncalos en el lugar o reinicia el culpable tras capturar diagnósticos suficientes.
- Recupera espacio de bajo riesgo: prune de caché de build; prune de contenedores detenidos; prune de imágenes no usadas si tu estrategia de despliegue lo permite.
- Valida la recuperación: verifica el espacio libre y que los servicios no estén en bucles de reintentos que generen más churn de disco.
Paso a paso: solución duradera (hazlo aburrido)
- Configura topes de logs de Docker globalmente (json-file max-size/max-file) y redeploy de contenedores.
- Mueve DockerRootDir fuera de particiones root pequeñas. Ponlo en almacenamiento dedicado dimensionado para artefactos y logs.
- Deja de escribir datos duraderos en capas escribibles: obliga a usar volúmenes para cualquier cosa persistente.
- Usa tmpfs para temporales reales: monta /tmp como tmpfs con límites de tamaño donde sea adecuado.
- Pon políticas de caché en el código: TTL + tamaño máximo + expulsión. “Lo limpiaremos luego” no es una política.
- Añade alertas: uso de filesystem del host, uso de DockerRootDir, uso por volumen, uso de inodos y anomalías en la tasa de logs.
- Operationaliza la limpieza: prune programado de caché de build en nodos CI/build con ventanas de retención y control de cambios.
Qué evitar (porque te sentirás tentado)
- Evita borrar directorios aleatorios bajo /var/lib/docker mientras Docker está en ejecución. Es una gran forma de corromper estado.
- Evita volume prune en producción a menos que tengas confirmación explícita de que los volúmenes no están en uso.
- Evita “rm -rf /tmp/*” si múltiples procesos comparten /tmp. Usa directorios de tmp propios.
- Evita tratar el prune como mantenimiento de un sistema que está generando escrituras sin control.
Preguntas frecuentes
1) ¿Por qué al borrar un archivo de log no se libera espacio?
Porque un proceso puede seguir escribiendo en un descriptor de archivo abierto incluso después de que la ruta se borre. El espacio se libera solo cuando el handle se cierra.
Usa lsof +L1 y reinicia/recarga el proceso.
2) ¿Debería cambiar Docker de json-file a journald?
Journald puede centralizar retención e integrarse con herramientas del sistema, pero no es una victoria automática. Si eliges journald, configura límites de journald.
Si mantienes json-file, fija max-size y max-file. Elige uno y géstionalo con intención.
3) ¿Es seguro docker system prune en producción?
“Seguro” depende de tus expectativas de rollout/rollback. Puede borrar imágenes no usadas y caché de build, lo que puede ralentizar redeploys o eliminar imágenes de rollback.
Prefiere prunes dirigidos (caché de builder, contenedores detenidos) y haz pruning agresivo en nodos CI, no en nodos críticos de producción.
4) ¿Cuál es la mejor forma de evitar que /tmp se llene?
Si el uso temporal es pequeño y realmente temporal, monta /tmp como tmpfs con un límite de tamaño. Si el uso temporal puede ser grande, usa un volumen dedicado con monitorización y limpieza.
En todos los casos, que la app escriba en un subdirectorio propio y limpia por antigüedad.
5) ¿Cómo sé si la capa escribible del contenedor es el problema?
Mira docker inspect SizeRw para contenedores, y dentro del contenedor ejecuta du -xhd1 /.
Si hay directorios grandes en ubicaciones que deberían ser volúmenes o tmpfs, encontraste el problema.
6) ¿Por qué las cachés de build se hacen tan grandes?
Los builds modernos generan muchas capas intermedias y objetos de caché. BuildKit es rápido porque almacena trabajo; también tiene hambre.
Prunea la caché de build en un cron, especialmente en builders compartidos y runners CI.
7) ¿Y el agotamiento de inodos—cómo lo prevengo?
Evita la creación de millones de archivos pequeños (diseños de caché que shardean en demasiados ficheros, tormentas de archivos temporales).
Monitorea el uso de inodos con df -hi. Coloca caches con muchos ficheros en filesystems preparados para ello o rediseña la disposición de la caché.
8) ¿Puedo imponer límites de disco por contenedor en Docker plano?
Docker no ofrece un “límite de disco” universal simple como CPU/memoria. Puedes aproximarlo con límites de tmpfs, volúmenes en filesystems con cuotas,
y guardarraíles operacionales (topes de logs, límites de caché). En Kubernetes, usa requests/limits de almacenamiento efímero.
9) ¿Los logs de la aplicación deben ir a stdout o a archivos?
Stdout/stderr suele ser la opción correcta para contenedores: recolección centralizada, rotación manejada por la plataforma, menos piezas móviles.
Los logs a archivos son aceptables por requisitos específicos, pero entonces eres responsable de rotación, retención y presupuesto de disco explícitamente.
Próximos pasos que puedes implementar esta semana
- Configurar rotación de logs de Docker globalmente (json-file max-size/max-file) y redeploy de los servicios más ruidosos primero.
- Instrumentar alertas de disco e inodos para el filesystem que aloja DockerRootDir y para tus volúmenes principales.
- Identificar los 3 principales consumidores de disco usando
docker system df -vydubajo DockerRootDir, luego asignar responsables. - Convertir “temporal” a tmpfs donde sea seguro con topes de tamaño explícitos. Haz que el fallo sea ruidoso, no contagioso.
- Poner expulsión de caché en código y probar que funciona bajo carga. Si no puedes explicar el tamaño máximo de tu caché, no tienes una caché.
- Programar prune de caché de build en nodos de builder/CI con una ventana de retención acorde al ritmo de desarrollo.
El objetivo no es scripts heroicos de limpieza. El objetivo es comportamiento de almacenamiento predecible. Los contenedores son rápidos de crear y fáciles de matar.
Tu disco no lo es.