Docker «No Space Left on Device»: Los lugares ocultos donde Docker consume tu disco

¿Te fue útil?

Estás de guardia. Una implementación falla. El CI está en rojo. Contenedores que funcionaban ayer de repente se niegan a arrancar con no space left on device. Te conectas por SSH, ejecutas df -h y tu disco parece… razonablemente bien. O peor: está lleno y no tienes idea de qué lo llenó porque “solo ejecutamos unos pocos contenedores”.

Docker es un gran mago. Hace aparecer aplicaciones. También hace desaparecer disco—silenciosamente, a través de múltiples capas de almacenamiento, registros, cachés y metadatos. El truco es saber dónde mirar y qué limpiezas son seguras en producción.

Guía rápida de diagnóstico

Este es el flujo “salirse del atasco en 10 minutos”. Prioriza las comprobaciones que te dicen si tienes un problema de bytes, de inodos, o una limitación específica del sistema de archivos (peculiaridades de overlay, metadatos de thinp, cuotas de proyecto).

Primero: confirma qué está lleno (bytes vs inodos vs un montaje)

  • Comprobar bytes libres: df -h en el montaje relevante (/, /var, /var/lib/docker y cualquier disco dedicado para datos de Docker).
  • Comprobar inodos: df -i. Si los inodos están al 100%, puedes tener “sin espacio” con gigabytes libres.
  • Confirmar Docker root: docker infoDocker Root Dir. La gente comprueba / y olvida que Docker está en /var (o viceversa).

Segundo: identifica qué categoría está creciendo

  • Contabilidad propia de Docker: docker system df -v para ver imágenes, contenedores, volúmenes y caché de compilación.
  • Realidad del sistema de archivos: du -xhd1 /var/lib/docker (o tu directorio root) para ver dónde viven realmente los bytes. Los números de Docker pueden retrasarse con respecto a la realidad, especialmente con los logs.
  • Registros: revisa los logs JSON de los contenedores o el uso de journald. Los logs son el enemigo número 1 de “no lo pensamos”.

Tercero: remediar en el orden menos destructivo

  1. Detener la hemorragia: rota logs, limita los drivers de logs o frena aplicaciones ruidosas.
  2. Liberar espacio seguro: prune de caché de compilación e imágenes colgantes. Evita eliminar volúmenes a menos que estés seguro.
  3. Abordar problemas estructurales: mover Docker root a un disco más grande, añadir monitorización, cuotas, retención de logs y arreglar la proliferación de builders en CI.

Chiste #1: El disco es como el minibar de un hotel—nadie recuerda usarlo hasta el checkout.

Qué significa realmente “no space left on device”

El mensaje es mentiroso por omisión. Puede significar:

  • No hay bloques libres en el sistema de archivos que respalda la capa escribible de Docker, un volumen o un directorio temporal.
  • No hay inodos libres (no puedes crear archivos nuevos aunque tengas espacio).
  • Se alcanzó una cuota (cuotas de proyecto, cuotas XFS o límites de metadatos del driver de almacenamiento).
  • Metadatos de thin pool llenos (común en setups antiguos con devicemapper).
  • Un montaje diferente está lleno del que comprobaste (p. ej., /var está lleno y / no).
  • Restricciones del sistema de archivos overlay que se manifiestan como errores de espacio (p. ej., demasiadas capas o el comportamiento de copy-up que explota el uso).

Operativamente: trátalo como “el kernel rechazó una asignación”. Tu trabajo es aprender qué asignación y dónde.

Una frase para pegar en un post-it en el centro de datos:

“La esperanza no es una estrategia.” — idea parafraseada frecuentemente atribuida al liderazgo de operaciones en círculos de fiabilidad

Si tu estrategia de gestión de discos es “prunearemos cuando duela”, ya estás funcionando con esperanza.

Los lugares ocultos donde Docker consume tu disco

1) Capas escribibles de contenedores (overlay2): muerte por escrituras pequeñas

Cada contenedor en ejecución tiene una capa escribible. En overlay2, es una estructura de directorios bajo algo como:

  • /var/lib/docker/overlay2 (común)
  • /var/lib/docker/containers (logs y configuración)

Las capas escribibles se inflan cuando las aplicaciones escriben en rutas que pensabas efímeras o externalizadas. Los clásicos:

  • Apps que escriben en /tmp dentro del contenedor y asumiste que estaba en memoria. No lo está, a menos que montes tmpfs.
  • Bases de datos que escriben en /var/lib/postgresql/data dentro del contenedor sin un volumen nombrado.
  • Manejadores de paquetes, runtimes de lenguajes o comprobaciones de actualización “útiles” que escriben caches en /root, /var/cache, /home.

El copy-up de overlay es una traición especial: leer un archivo de una capa inferior es barato; modificarlo fuerza una copia en la capa escribible. Tocar un archivo grande puede duplicarlo. Así es como “solo hicimos un pequeño cambio de configuración” se convierte en gigabytes.

2) Archivos de registro JSON: el hog ganador del disco

El logging por defecto de Docker (json-file) escribe logs por contenedor en:

  • /var/lib/docker/containers/<container-id>/<container-id>-json.log

Si no configuras rotación de logs, esos archivos crecen para siempre. Y “para siempre” en producción se mide en incidentes.

Chiste #2: Lo único que escala automáticamente sin aprobación de presupuesto es el volumen de tus logs.

3) Volúmenes nombrados: duraderos por diseño, olvidados por hábito

Los volúmenes son donde el estado va a vivir. También sobreviven a la eliminación de contenedores. Ese es el punto y también la trampa.

La proliferación de volúmenes ocurre cuando:

  • Usas nombres de volúmenes autogenerados en Compose o CI y nunca los limpias.
  • Ejecutas stacks efímeros de pruebas que crean volúmenes por ejecución.
  • Montas mal y terminas escribiendo en un volumen nombrado que no pretendías.

Matiz importante: eliminar imágenes raramente libera espacio de volúmenes. La gente hace “docker rmi todo” y sigue con disco lleno porque los volúmenes son el disco.

4) Caché de compilación: BuildKit es rápido porque recuerda todo

Las compilaciones modernas de Docker (especialmente con BuildKit) almacenan en caché capas agresivamente. Es fantástico para la velocidad del CI. También es como los nodos de build se convierten en vertederos de disco.

La caché de compilación crece por:

  • Builds multi-stage con muchos pasos que invalidan frecuentemente.
  • Muchas ramas y tags que generan capas únicas.
  • Descargas del manejador de paquetes cacheadas en capas, multiplicadas por variaciones.

5) Imágenes no usadas: el museo de «quizá necesitemos ese tag»

Las imágenes se acumulan cuando los nodos actúan tanto como hosts de runtime como de build, o cuando tu estrategia de despliegue tira muchas versiones y nunca las elimina. En clusters, cada nodo se vuelve su propio museo privado de capas “potencialmente útiles algún día”.

6) Contenedores huérfanos y capas muertas: restos tras crashes y upgrades

En operación normal Docker limpia. En operación anormal—caídas del daemon, reboots forzados, drivers de almacenamiento rotos—la basura puede persistir. Además: algunos patrones de orquestación crean y abandonan contenedores a una tasa sorprendente.

7) Directorios temporales fuera del Docker root: emboscadas en /tmp y /var/tmp

Docker usa archivos temporales durante pulls, builds y descompresiones. Dependiendo de la configuración y variables de entorno, el uso temporal puede aterrizar en:

  • /tmp o /var/tmp en el host
  • un tmp privado de systemd para el servicio docker

Así que puedes llenar /tmp y hacer caer servicios no relacionados, aun cuando /var/lib/docker esté en un disco separado.

8) Journald: “pero no usamos json-file” (sí, todavía tienes logs)

Si Docker registra en journald, los logs pueden no estar bajo el directorio de Docker en absoluto. Se acumularán en el almacenamiento de journald (a menudo bajo /var/log/journal), gobernados por la retención de journald. Genial, hasta que tus valores por defecto de logging son “guardar todo” y tus discos son “pequeños”.

9) Casos límite del driver de almacenamiento: metadatos devicemapper, cuotas de proyecto XFS y amigos

La mayoría de instalaciones modernas usan overlay2 sobre ext4 o XFS. Pero entornos antiguos (o “sistemas heredados cuidadosamente preservados”) aún pueden usar devicemapper. En devicemapper es común alcanzar límites de metadatos antes que límites reales de disco—resultando en “no space” cuando el disco no está lleno.

Las cuotas de proyecto XFS también pueden sorprender: Docker puede configurarse para aplicar límites por contenedor. Genial cuando es intencional, confuso cuando viene heredado de una AMI que no auditaste.

Tareas prácticas: comandos, salidas y decisiones

Estos son comandos reales y ejecutables. Cada uno incluye qué significa la salida y la decisión que tomas a partir de ella. Úsalos en orden, no al azar como un mapache en una sala de servidores.

Tarea 1: Identificar el sistema de archivos lleno

cr0x@server:~$ df -h
Filesystem                         Size  Used Avail Use% Mounted on
/dev/nvme0n1p2                      80G   62G   14G  82% /
/dev/nvme1n1p1                     200G  196G  4.0G  99% /var/lib/docker
tmpfs                               16G  1.2G   15G   8% /run

Significado: Tu disco de datos de Docker está lleno (/var/lib/docker al 99%). El filesystem root no es el problema principal.

Decisión: Enfócate en el uso del directorio root de Docker; no pierdas tiempo limpiando /.

Tarea 2: Comprobar agotamiento de inodos (el engañoso error de “espacio”)

cr0x@server:~$ df -i
Filesystem                        Inodes   IUsed   IFree IUse% Mounted on
/dev/nvme0n1p2                   5242880  841120 4401760   17% /
/dev/nvme1n1p1                  13107200 13107200       0  100% /var/lib/docker

Significado: El sistema de archivos de Docker se quedó sin inodos, no sin bloques. Esto suele ocurrir con millones de archivos pequeños (node_modules, extracción de capas de imágenes, caches de build).

Decisión: El pruning puede ayudar, pero a largo plazo probablemente necesites un sistema de archivos creado con mayor densidad de inodos (ext4) o cambiar a XFS (inodos dinámicos) y reducir el churn de archivos pequeños.

Tarea 3: Confirmar el directorio root real de Docker

cr0x@server:~$ docker info --format '{{.DockerRootDir}}'
/var/lib/docker

Significado: Docker confirma que usa /var/lib/docker.

Decisión: Todos los análisis posteriores de disco deben apuntar a esta ruta (salvo que uses un runtime alternativo o Docker rootless).

Tarea 4: Obtener la contabilidad de espacio de alto nivel de Docker

cr0x@server:~$ docker system df
TYPE            TOTAL     ACTIVE    SIZE      RECLAIMABLE
Images          48        12        72.4GB    41.8GB (57%)
Containers      18        7         3.1GB     2.2GB (71%)
Local Volumes   64        9         88.6GB    55.0GB (62%)
Build Cache     214       0         61.3GB    61.3GB

Significado: Volúmenes y caché de compilación dominan. No es principalmente “demasiados contenedores”.

Decisión: Comienza con prune de caché de compilación (normalmente seguro), luego audita volúmenes cuidadosamente antes de hacer prune.

Tarea 5: Profundizar con contabilidad verbose de Docker

cr0x@server:~$ docker system df -v
Images space usage:
REPOSITORY   TAG      IMAGE ID       CREATED        SIZE      SHARED SIZE   UNIQUE SIZE   CONTAINERS
app/api      prod     2a1b3c4d5e6f   2 days ago     1.21GB    820MB         390MB         4
app/api      old      7f6e5d4c3b2a   3 weeks ago    1.18GB    820MB         360MB         0

Build cache usage:
CACHE ID    CACHE TYPE    SIZE     CREATED         LAST USED      USAGE     SHARED
k9x...      regular       2.3GB    2 weeks ago     2 weeks ago    1
...

Significado: Puedes detectar tags de imágenes sin uso (0 contenedores) y caches antiguas.

Decisión: Elimina imágenes y caches sin uso primero; considera políticas para mantener solo N versiones por nodo.

Tarea 6: Identificar los directorios más grandes bajo Docker root (comprobación de la realidad)

cr0x@server:~$ sudo du -xhd1 /var/lib/docker | sort -h
1.1G    /var/lib/docker/network
3.6G    /var/lib/docker/containers
62G     /var/lib/docker/buildkit
112G    /var/lib/docker/overlay2
181G    /var/lib/docker

Significado: Overlay2 y buildkit son los grandes culpables. El directorio containers no es trivial (a menudo logs).

Decisión: Si containers es grande, inspecciona logs. Si buildkit es grande, prune de caché de compilación. Overlay2 requiere limpieza cuidadosa vía Docker, no borrado manual.

Tarea 7: Encontrar los archivos de log más grandes de los contenedores (driver json-file)

cr0x@server:~$ sudo find /var/lib/docker/containers -name "*-json.log" -printf "%s %p\n" | sort -nr | head
21474836480 /var/lib/docker/containers/4c2.../4c2...-json.log
 9876543210 /var/lib/docker/containers/91a.../91a...-json.log
 1234567890 /var/lib/docker/containers/ab7.../ab7...-json.log

Significado: Un contenedor escribió ~20GB de logs. Eso no es “un poco charla”. Eso es una notificación de desalojo del disco.

Decisión: Trunca inmediatamente ese log (seguro a corto plazo), luego implementa rotación y corrige la app ruidosa.

Tarea 8: Truncar de forma segura un log de contenedor sobredimensionado sin reiniciar Docker

cr0x@server:~$ sudo truncate -s 0 /var/lib/docker/containers/4c2.../4c2...-json.log
cr0x@server:~$ sudo ls -lh /var/lib/docker/containers/4c2.../4c2...-json.log
-rw-r----- 1 root root 0 Jan  2 11:06 /var/lib/docker/containers/4c2.../4c2...-json.log

Significado: Recuperaste espacio de inmediato; el archivo ahora está vacío. El contenedor continúa registrando.

Decisión: Trata esto como un parche de emergencia. Programa la solución adecuada: opciones de logging, elección de driver de logs o reducción de logs a nivel de aplicación.

Tarea 9: Confirmar qué contenedor mapea al directorio de logs ruidoso

cr0x@server:~$ docker ps --no-trunc --format 'table {{.ID}}\t{{.Names}}'
CONTAINER ID                                                       NAMES
4c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b   api-prod-1

Significado: El mayor ofensor de logs es api-prod-1.

Decisión: Revisa el nivel de logs de la app, picos de solicitudes, reintentos o bucles de error. Los problemas de disco suelen ser solo un síntoma de una falla aguas arriba.

Tarea 10: Comprobar uso de disco de journald (si usas journald como driver de logs)

cr0x@server:~$ journalctl --disk-usage
Archived and active journals take up 18.7G in the file system.

Significado: Journald está consumiendo espacio significativo. Esto puede ser logs de Docker, logs del sistema o ambos.

Decisión: Establece límites de retención en la configuración de journald y elimina logs antiguos con las herramientas de journald. No borres simplemente archivos bajo /var/log/journal mientras journald está en ejecución.

Tarea 11: Vaciar logs de journald para recuperar espacio

cr0x@server:~$ sudo journalctl --vacuum-size=2G
Deleted archived journal /var/log/journal/7a1.../system@000...-000...journal
Vacuuming done, freed 16.7G of archived journals on disk.

Significado: Se recuperó espacio de forma segura mediante las herramientas de journald.

Decisión: Implementa una política persistente de journald (límites de tamaño/tiempo) para que esto no vuelva la próxima semana.

Tarea 12: Prune de caché de compilación (habitualmente bajo riesgo, alta recompensa)

cr0x@server:~$ docker builder prune --all --force
Deleted build cache objects:
k9x...
m2p...
Total reclaimed space: 59.8GB

Significado: Recuperaste casi 60GB al eliminar caché de compilación. Las builds pueden ser más lentas hasta que la caché se caliente nuevamente.

Decisión: Si esto es un builder de CI, programa prune periódico o limita la caché con políticas en lugar de “prunear en pánico”.

Tarea 13: Prune de imágenes no usadas (más o menos seguro, pero entiende tu estrategia de despliegue)

cr0x@server:~$ docker image prune -a --force
Deleted Images:
deleted: sha256:7f6e5d4c3b2a...
deleted: sha256:1a2b3c4d5e6f...
Total reclaimed space: 28.4GB

Significado: Docker eliminó imágenes no referenciadas por ningún contenedor. Si dependes de rollback rápido manteniendo imágenes antiguas localmente, acabas de eliminar tu red de seguridad.

Decisión: En nodos de producción, considera mantener las últimas N versiones o depender de un registry con buena disponibilidad y caché.

Tarea 14: Encontrar volúmenes grandes y quién los usa

cr0x@server:~$ docker volume ls
DRIVER    VOLUME NAME
local     api_db_data
local     prometheus_data
local     tmp_ci_run_1738

cr0x@server:~$ sudo du -sh /var/lib/docker/volumes/*/_data | sort -h | tail
6.2G  /var/lib/docker/volumes/prometheus_data/_data
48G   /var/lib/docker/volumes/api_db_data/_data
71G   /var/lib/docker/volumes/tmp_ci_run_1738/_data

Significado: Un volumen “tmp” de CI ocupa 71GB. Probablemente basura. El volumen DB es grande pero probablemente legítimo.

Decisión: Audita el adjunto antes de eliminar: identifica qué contenedores usan el volumen tmp. No borres volúmenes de base de datos a la ligera.

Tarea 15: Mapear volúmenes a contenedores (evita borrar estado en uso)

cr0x@server:~$ docker ps -a --format '{{.ID}} {{.Names}}' | head
a1b2c3d4e5f6 api-prod-1
d4e5f6a1b2c3 ci-runner-1738
...

cr0x@server:~$ docker inspect -f '{{.Name}} -> {{range .Mounts}}{{.Name}} {{end}}' d4e5f6a1b2c3
/ci-runner-1738 -> tmp_ci_run_1738

Significado: El volumen tmp grande pertenece a un contenedor CI runner específico (posiblemente ya detenido, posiblemente aún en uso).

Decisión: Si el contenedor está detenido y el volumen es verdaderamente efímero, elimina contenedor y volumen. Si está en ejecución, arregla el job que está escribiendo tanto.

Tarea 16: Eliminar un volumen huérfano confirmado

cr0x@server:~$ docker rm -f ci-runner-1738
ci-runner-1738

cr0x@server:~$ docker volume rm tmp_ci_run_1738
tmp_ci_run_1738

Significado: Contenedor y volumen eliminados; el espacio en disco debería disminuir.

Decisión: Añade automatización de ciclo de vida para artefactos CI para que “volúmenes tmp” no se conviertan en residentes permanentes.

Tarea 17: Un comando para limpiar la basura obvia (usar con criterio)

cr0x@server:~$ docker system prune --all --volumes --force
Deleted Containers:
...
Deleted Images:
...
Deleted Volumes:
...
Total reclaimed space: 132.6GB

Significado: Acabas de eliminar básicamente todo lo no usado, incluidos volúmenes. Esto puede ser catastrófico si clasificaste mal “no usado”.

Decisión: Usa esto solo en hosts desechables (CI, builders de desarrollo) o cuando hayas verificado la seguridad de los volúmenes. En producción, prefiere pruning dirigido.

Tarea 18: Mover el directorio root de Docker a un disco más grande (la solución adulta)

Cuando estás constantemente haciendo prune, estás tratando síntomas. A veces necesitas mover los datos.

cr0x@server:~$ sudo systemctl stop docker
cr0x@server:~$ sudo rsync -aHAX --numeric-ids /var/lib/docker/ /mnt/docker-data/
cr0x@server:~$ sudo mkdir -p /etc/docker
cr0x@server:~$ sudo tee /etc/docker/daemon.json > /dev/null
{
  "data-root": "/mnt/docker-data"
}
cr0x@server:~$ sudo systemctl start docker
cr0x@server:~$ docker info --format '{{.DockerRootDir}}'
/mnt/docker-data

Significado: Docker ahora usa el nuevo data root. Si los contenedores fallan al iniciar, probablemente perdiste permisos, contextos SELinux o flags de rsync.

Decisión: Esto es una operación de control de cambios. Hazla en una ventana de mantenimiento y valida con un contenedor canario primero.

Tres micro-historias corporativas desde las trincheras

Micro-historia #1: El incidente causado por una suposición errónea (los logs “no pueden ser tan grandes”)

La compañía estaba en migración de VMs a contenedores. El servicio core había sido estable durante años y la contenedorización fue deliberadamente mínima: “lift and shift, no refactorizar.” Esa decisión no estuvo mal. La suposición ligada a ella sí.

Asumieron que los logs “los manejaba la plataforma” porque la antigua imagen VM tenía logrotate. En Docker, la app seguía escribiendo a stdout/stderr. La plataforma lo manejó—escribiendo logs JSON a disco, para siempre, sin rotación. El primer día estuvo bien. Al día veinte, un nodo empezó a devolver 500s. El orquestador siguió reprogramando, porque “los contenedores son ganado.” Bien. El nodo se mantuvo lleno porque reprogramar no eliminó los archivos de log lo suficientemente rápido, y los nuevos contenedores siguieron registrando en el mismo abismo.

El ingeniero de guardia comprobó df -h en /, vio 40% libre y declaró “no es disco.” Pasó por alto que Docker vivía en /var, y /var era un montaje distinto. Un segundo ingeniero ejecutó docker system df y no vio nada escandaloso—porque la contabilidad de Docker no gritaba “un archivo de log tiene 20GB”.

La solución fue brutalmente simple: truncar el archivo de log, limitar el tamaño de los logs y bajar el nivel de logs para un bucle caliente que había sido inofensivo en VMs porque los logs rotaban. La acción post-incidente fue también simple y más importante: documentar dónde viven los logs para cada driver de logging y alertar por crecimiento. Esto es lo que realmente significa “trabajo de plataforma”.

Micro-historia #2: La optimización que salió mal (caché de BuildKit por todas partes)

Un equipo distinto estaba orgulloso de su velocidad de CI. Las builds duraban pocos minutos, en gran parte porque la caché de BuildKit funcionaba perfectamente. Demasiado perfecta. Sus builders también ejecutaban algunos servicios de larga vida (porque “teníamos capacidad sobrante”), y los builders tenían SSDs locales grandes. Parecía eficiente: una clase de máquina, una imagen golden, todo programado en cualquier lado.

La caché creció en silencio. Builds multi-arquitectura, actualizaciones frecuentes de dependencias y el hábito de taggear cada commit crearon una caché de alta rotación. No importó por una semana. Entonces una rama de lanzamiento grande produjo una avalancha de builds y variantes de capas. La caché se infló y empujó el disco al límite durante horario laboral.

Lo doloroso no fue el disco lleno. Lo doloroso fue el efecto de segundo orden: al llenarse el disco, los builders se ralentizaron, los jobs timeoutearon, los reintentos aumentaron la carga y la caché creció aún más rápido. El sistema se volvió un bucle autoalimentado: la “optimización” hizo que la falla fuera más explosiva.

La solución final no fue “prunear más.” Separaron roles: builders dedicados con limitación programada de caché, runtimes dedicados con retención estricta de imágenes y límites explícitos para logs. También dejaron de pretender que “build rápido” es el mismo KPI que “build estable”.

Micro-historia #3: La práctica aburrida pero correcta que salvó el día (cuotas y alertas)

Un equipo de plataforma interno orientado a finanzas tenía un hábito impopular: ponían cuotas y umbrales de alerta en todo. Los desarrolladores se quejaban, porque las cuotas se sienten burocracia hasta que entiendes el radio de blast.

Configurar ong rotación de logs para el driver json-file de Docker y además poner topes en journald en hosts que usan journald. Pusieron alertas en uso de /var/lib/docker, uso de inodos y en los N archivos de log de contenedor más grandes. El ruido de alertas fue bajo porque los umbrales estuvieron afinados y las alertas tenían runbooks asociados.

Una noche de viernes, un servicio empezó a spamear un mensaje de error por un problema de rotación de credenciales downstream. En las plataformas de otros equipos, ese tipo de incidente se convierte en “disco lleno” más “app caída”. En la plataforma de este equipo, los archivos de log alcanzaron su tope, los logs rotaron, el disco se mantuvo sano y el de guardia recibió una alerta: “aumento de tasa de errores del servicio + aumento de volumen de logs.” Arreglaron el problema de credenciales. No hubo pánico por limpieza. No hubo investigación de sistema de archivos. La fiabilidad aburrida ganó, otra vez.

Errores comunes: síntoma → causa raíz → solución

1) “df muestra espacio libre, pero Docker dice no space”

Síntoma: Pull/build/start falla con no space left on device; df -h en / muestra bastante libre.

Causa raíz: El root de Docker está en un montaje diferente (/var o disco dedicado), o estás llenando /tmp durante builds.

Solución: docker info para Docker Root Dir; ejecuta df -h en ese montaje y en /tmp. Mueve data-root o expande el filesystem correcto.

2) “No space” pero tienes gigabytes libres

Síntoma: Escrituras fallan; df -h muestra GBs libres; los errores persisten.

Causa raíz: Agotamiento de inodos (df -i muestra 100%) o metadatos de thin pool llenos (devicemapper).

Solución: Si son inodos: prune de caches con muchos archivos pequeños y recrea el filesystem con densidad de inodos adecuada (o usa XFS). Si devicemapper: migra a overlay2 o expande metadatos de thin pool.

3) “docker system prune no liberó nada”

Síntoma: Hiciste prune, pero el uso de disco casi no cambió.

Causa raíz: El culpable son los logs o journald, o grandes volúmenes nombrados adjuntos a contenedores en ejecución.

Solución: Inspecciona /var/lib/docker/containers y el uso de journald; revisa tamaños de volúmenes bajo /var/lib/docker/volumes y mapea volúmenes a contenedores.

4) “Borramos contenedores, pero el disco no bajó”

Síntoma: Eliminar contenedores no libera el espacio esperado.

Causa raíz: Los volúmenes persisten; las imágenes persisten; la caché de build persiste; además, archivos eliminados pero abiertos pueden mantener espacio asignado hasta que el proceso salga.

Solución: Revisa volúmenes y caché de compilación; si sospechas archivos eliminados pero abiertos, reinicia al culpable (a veces el daemon Docker o el runtime) tras una limpieza segura.

5) “El directorio overlay2 es enorme; ¿podemos borrarlo?”

Síntoma: /var/lib/docker/overlay2 domina el uso de disco.

Causa raíz: Ahí viven capas de imagen y capas escribibles. El borrado manual rompe el estado de Docker.

Solución: Usa comandos Docker para prune de imágenes/contenedores no usados; si el estado está corrupto, planifica un borrado controlado y recreación para hosts desechables, no para nodos stateful en producción.

6) “Después de cambiar a journald, el disco sigue llenándose”

Síntoma: Cambiaste el driver de logs; el uso de disco continúa creciendo.

Causa raíz: Los valores por defecto de retención de journald son demasiado permisivos o el almacenamiento persistente del journal está habilitado sin topes.

Solución: Configura límites de tamaño/tiempo en journald y valida con journalctl --disk-usage.

7) “Los builders de CI se quedan sin disco semanalmente”

Síntoma: Los nodos builder se llenan de forma predecible.

Causa raíz: Retención de caché de BuildKit sin límites; múltiples toolchains generan muchas capas únicas; demasiados tags/branches construidos en el mismo nodo.

Solución: docker builder prune programado; separar builder y runtime; imponer retención y/o reconstruir builders periódicamente (la infraestructura inmutable ayuda aquí).

8) “Se liberó espacio, pero el servicio sigue roto”

Síntoma: Recuperaste disco, pero los contenedores aún fallan al arrancar o se comportan de forma errática.

Causa raíz: Metadatos de Docker corruptos, pulls parciales o fallo a nivel de aplicación que originalmente causó logs excesivos.

Solución: Valida con un contenedor conocido bueno, revisa logs del daemon y arregla el problema aguas arriba (limitación de tasa, tormenta de reintentos, fallo de autenticación). El disco fue solo daño colateral.

Listas de verificación / plan paso a paso

Lista de emergencia (nodo de producción está lleno ahora mismo)

  1. Confirma el montaje: ejecuta df -h y df -i en Docker root y en /tmp.
  2. Detén primero los logs descontrolados: encuentra los archivos de log de contenedor más grandes; trunca los peores; reduce el nivel de logs si es seguro.
  3. Recupera caché seguro: ejecuta docker builder prune --all en nodos builders; ejecuta docker image prune -a si comprendes el impacto en rollback.
  4. Audita volúmenes antes de tocarlos: identifica los volúmenes más grandes y mapea a contenedores. Elimina solo volúmenes huérfanos confirmados.
  5. Verifica espacio libre: vuelve a ejecutar df -h. Mantén al menos unos gigabytes libres; algunos sistemas de archivos y daemons se comportan mal cerca del 100%.
  6. Estabiliza: reinicia componentes que fallan solo después de aliviar la presión de disco; evita flapping.
  7. Escribe la nota del incidente: qué llenó el disco, qué tan rápido creció y qué cambio de política lo previene.

Lista de endurecimiento (hacer que deje de pasar)

  1. Configurar rotación de logs de Docker: limita tamaño y número para logs json-file.
  2. Configurar retención de journald: limita almacenamiento y/o tiempo si usas journald.
  3. Separar responsabilidades: builders y runtimes no deberían ser la misma flota salvo que te guste el crecimiento misterioso.
  4. Establecer políticas de prune: prune programado de caché de compilación y reglas de retención de imágenes por rol del host.
  5. Mover Docker root a almacenamiento dedicado: especialmente en filesystems root pequeños.
  6. Alertar sobre inodos y bytes: e incluir runbooks que apunten a estos comandos exactos.
  7. Medir a los mayores ofensores: mayores volúmenes, logs de contenedor más grandes, imágenes más pesadas por host.
  8. Diseñar para fallar: si un downstream rompe y desencadena tormentas de reintentos, tu plataforma debería degradarse sin autodestruirse.

Configuración recomendada base del daemon Docker (valores prácticos)

Si usas el driver json-file, configura rotación de logs. Esta es la medida de control de disco más rentable que puedes aplicar.

cr0x@server:~$ sudo tee /etc/docker/daemon.json > /dev/null
{
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "50m",
    "max-file": "5"
  }
}
cr0x@server:~$ sudo systemctl restart docker

Significado: El archivo de log de cada contenedor rota a ~50MB, manteniendo 5 archivos (~250MB por contenedor en el peor caso).

Decisión: Ajusta tamaños según el entorno. En producción suele ser necesario logging centralizado; los logs locales deben ser un buffer, no un archivo histórico.

Datos interesantes y contexto histórico

  • Dato 1: Los despliegues tempranos de Docker solían usar el modo devicemapper loopback por defecto, que era lento y propenso a fallos “misteriosos” de espacio/metadatos bajo carga.
  • Dato 2: El cambio de Docker a overlay2 como defecto común hizo el almacenamiento más rápido y simple, pero también convirtió el comportamiento copy-up en una sorpresa frecuente para equipos que escriben en filesystem de contenedores.
  • Dato 3: El driver de logs por defecto de Docker históricamente fue json-file, optimizado para simplicidad, no para higiene de disco a largo plazo.
  • Dato 4: La popularidad de BuildKit subió porque hacía builds más rápidas y paralelas, pero el coste operacional es la gestión de caché—especialmente en builders compartidos.
  • Dato 5: La frase “no space left on device” es un errno genérico (ENOSPC) devuelto por el kernel, y se usa para más cosas que “el disco está lleno”.
  • Dato 6: El agotamiento de inodos es un viejo problema Unix que nunca murió; los contenedores lo trajeron de vuelta porque la extracción de imágenes y algunos ecosistemas de lenguajes generan muchísimos archivos pequeños.
  • Dato 7: Muchos operadores aprendieron a la mala que “los contenedores son efímeros” no es una afirmación sobre datos. Los volúmenes son estado, y el estado es para siempre a menos que lo borres.
  • Dato 8: La propia contabilidad de espacio de Docker (docker system df) es útil pero no autoritativa; el sistema de archivos es la verdad, especialmente para logs y uso temporal fuera de Docker.

Preguntas frecuentes

1) ¿Por qué Docker dice “no space left on device” cuando df -h muestra espacio?

Porque comprobaste el montaje equivocado, o te quedaste sin inodos, o alcanzaste una cuota/límite de metadatos. Siempre comprueba el directorio root de Docker y ejecuta df -i.

2) ¿Es seguro ejecutar docker system prune -a en producción?

A veces. Elimina imágenes, contenedores y redes no usadas. Puede romper estrategias de rollback rápido y causar redeploys más lentos por pulls de imágenes. Usa prune dirigido primero.

3) ¿Es seguro ejecutar docker system prune --volumes?

Sólo si has verificado que los volúmenes “no usados” son realmente desechables. “No usado” significa “no referenciado actualmente”, no “sin importancia”. Así es como borras datos.

4) ¿Por qué mis logs de contenedor son enormes?

Porque el logging por defecto json-file es ilimitado a menos que configures max-size y max-file. Además, una app ruidosa puede generar gigabytes por hora durante bucles de error.

5) Si trunco los logs de un contenedor, ¿se romperá Docker o la app?

Truncar el archivo de log JSON suele ser seguro como medida de emergencia. Pierdes logs históricos y la app sigue registrando. Luego arregla la rotación correctamente.

6) ¿Por qué eliminar un contenedor no libera espacio?

Porque el espacio probablemente está en volúmenes, imágenes o caché de compilación. Además, el espacio de archivos borrados puede permanecer asignado si un proceso todavía tiene el archivo abierto.

7) ¿Por qué /var/lib/docker/overlay2 es tan grande aunque no tenga muchas imágenes?

Overlay2 incluye capas escribibles y contenidos extraídos de capas. Unas pocas imágenes “grandes” más contenedores con mucha escritura pueden dominar fácilmente el disco.

8) ¿Cuál es la mejor forma de prevenir incidentes de disco de Docker en builders de CI?

Builders dedicados, docker builder prune programado, cachés acotados y reconstrucción periódica de builders. Trata las cachés como consumibles, no como tesoros.

9) ¿Puedo simplemente borrar archivos bajo /var/lib/docker manualmente?

No. El borrado manual suele corromper la visión que Docker tiene del mundo. Usa comandos Docker o haz un borrado controlado sólo en hosts realmente desechables.

10) ¿Cuánto espacio libre debería mantener en un host Docker?

Suficiente para que pulls/descompresiones y picos de logs no te lleven al 100%. Prácticamente: mantén un buffer de varios gigabytes y alerta con anticipación antes del precipicio.

Conclusión: pasos siguientes que realmente previenen repeticiones

Cuando Docker se queda sin espacio, rara vez es “Docker es grande” y casi siempre es “no gestionamos las partes aburridas”. Logs, cachés y volúmenes son aburridos. También son de donde vienen los incidentes.

Tus próximos pasos prácticos:

  1. Imponer un tope en los logs hoy (rotación json-file y/o retención de journald). Esto por sí solo elimina una gran clase de outages.
  2. Definir roles de host: nodos de runtime no deben acumular cachés de compilación; los builders deben tener prune programado y rebuilds predecibles.
  3. Alertar sobre bytes e inodos del filesystem root de Docker, además de los tamaños de log de contenedor top y los volúmenes más grandes.
  4. Dejar de escribir estado en capas escribibles: usa volúmenes intencionalmente, monta tmpfs para datos temporales reales y audita las rutas que tus apps escriben.
  5. Cuando limpies, sé quirúrgico: cachés e imágenes no usadas primero, volúmenes sólo con evidencia.

El disco no es glamuroso. Por eso gana tantas peleas. Haz que sea trabajo de alguien—preferiblemente tuyo—antes de que sea tu fin de semana.

← Anterior
Proxmox «dpkg fue interrumpido»: Reparar paquetes rotos sin reinstalar
Siguiente →
Programación de scrub de ZFS: cómo evitar dolores en horas pico

Deja un comentario