Los peores incidentes con Docker no parecen dramáticos al principio. Un contenedor “funciona” hasta que deja de hacerlo, y cuando falla suele hacerlo de forma lateral:
la latencia sube, los reintentos se disparan, los discos se llenan, los nodos empiezan a usar swap, y entonces todo es “no relacionado” hasta que todo está relacionado.
La solución no es “añadir más dashboards”. La solución es un mínimo implacable: un conjunto pequeño de métricas y logs que se encienden de forma fiable antes que los usuarios,
y una forma de interrogar un host en minutos cuando los dashboards mienten o faltan.
La señal mínima: qué observar y por qué
“Observabilidad” en el mundo de contenedores se vende como un buffet ilimitado: traces, profiles, eBPF, mapas de servicios, flame graphs sofisticados,
detección de anomalías con IA y un dashboard con doce tonos de rojo. En producción, la observabilidad mínima viable es más simple:
detectar modos de fallo inminentes temprano, con señales difíciles de falsear y baratas de recolectar.
Para Docker en un host único o en una flota, estás manejando tres capas que fallan de forma distinta:
- Capa de aplicación: errores en requests, latencia, reintentos, timeouts, profundidad de colas. Aquí es donde los usuarios gritan.
- Capa de contenedor: reinicios, OOM kills, CPU throttling, agotamiento de descriptores de archivo, volumen de logs.
- Capa de host: llenado de disco/inodos, saturación de IO, presión de memoria/swap, agotamiento de conntrack, problemas del kernel.
El conjunto mínimo debe darte advertencia temprana para lo predecible: disco llenándose, fuga de memoria, CPU throttling, crash loops,
y los asesinos silenciosos como agotamiento de inodos y conntrack. Si dominas esos, previenes la mayoría de los incidentes “ayer funcionaba”.
Reglas del mínimo
- Prefiere indicadores líderes. “Disco 98% lleno” es indicador rezagado. “Disco creciendo 2% por hora” es un indicador líder.
- Elige una fuente de verdad por cosa. Para CPU, usa uso de CPU de cgroup y throttling. No mezcles cinco definiciones de “CPU%”.
- Mantén la cardinalidad bajo control. Etiqueta cada métrica con IDs de contenedor y harás un DoS a tu monitoreo por éxito.
- Alerta sobre síntomas en los que puedas actuar. “Load average alto” es trivia a menos que vaya ligado a CPU steal, IO wait o throttling.
Existen dos tipos de equipos: los que alertan por “contenedor reiniciado” y los que disfrutan siendo sorprendidos por eso a las 2 a.m.
Además, una verdad seca: si tu stack de “observabilidad” cae cada vez que el cluster está mal, no es observabilidad; es decoración.
Tu mínimo necesita ser accesible desde el host con un shell.
Hechos interesantes y contexto histórico (las partes que aún duelen)
- cgroups se introdujeron en Linux en 2007. Docker no inventó el aislamiento de recursos; lo empaquetó y facilitó su mal uso.
- El driver de logging original de Docker por defecto era json-file. Es conveniente, pero en servicios con mucho tráfico puede convertirse en tu devorador sigiloso de disco.
- OverlayFS (overlay2) se convirtió en el driver de almacenamiento por defecto en muchas distros en 2016–2017. Es suficientemente rápido, pero “¿dónde quedó mi disco?” se volvió una pregunta semanal.
- El comportamiento del OOM killer existe desde décadas antes de los contenedores. Los contenedores solo facilitaron alcanzar límites de memoria y hicieron más difícil ver por qué sin las señales correctas.
- Conntrack ha sido una característica de netfilter de Linux desde los kernels 2.4 tempranos. Cuando NAT y muchas conexiones de corta duración se encuentran con contenedores, conntrack pasa a ser un asunto de capacidad, no una nota al pie.
- Healthchecks en Dockerfile se añadieron tras dolores tempranos en producción. Antes de eso, “el contenedor está corriendo” se trataba como “el servicio está saludable”, lo cual es adorable.
- systemd-journald tiene limitación de tasa por una razón. Loggear es IO; IO es latencia; la latencia se vuelve reintentos; los reintentos se vuelven una tormenta. Los logs pueden derribarte.
- CPU throttling no es “CPU alta”. Es “pediste CPU y el kernel dijo que no.” Esta distinción se hizo más visible con la contenedorización generalizada.
Conjunto mínimo de métricas (por modo de fallo)
No empieces por “coleccionar todo”. Empieza por cómo los sistemas Docker realmente mueren. A continuación hay un mínimo que detecta fallas temprano
y te da una dirección para depurar. Esto asume que puedes recolectar métricas del host (node exporter o equivalente), métricas de contenedor
(cAdvisor o Docker API), y métricas básicas de la app (estadísticas HTTP/gRPC). Incluso si no usas Prometheus, los conceptos aplican.
1) Crash loops y despliegues malos
- Tasa de recuento de reinicios de contenedores (por servicio, no por ID de contenedor). Alerta por reinicios sostenidos durante 5–10 minutos.
- Distribución de códigos de salida. Exit 137 suele ser OOM kill; exit 1 suele ser crash de app; exit 143 es SIGTERM (a menudo normal durante deploy).
- Fallos de healthcheck (si usas HEALTHCHECK). Alerta antes de los reinicios, porque la falla de health es tu indicador líder.
Decisión que quieres habilitar: rollback o detener la hemorragia. Si la tasa de reinicios sube tras un deploy, no “esperes a que se estabilice.”
No lo hará. Los contenedores no son plantas de interior.
2) Fugas de memoria, presión de memoria y OOM kills
- Working set de memoria del contenedor (no solo RSS, y no cache a menos que sepas lo que haces).
- Eventos OOM kill a nivel de host y contenedor.
- Memoria disponible del host y swap in/out. La actividad de swap es la película de desastre en cámara lenta.
Umbrales de alerta: working set acercándose al límite (por ejemplo, > 85% durante 10 minutos), OOM kills > 0 (página inmediata), tasa de swap-in > 0 sostenida (aviso).
3) Disco: el generador de outages número 1 y aburrido
- % libre del sistema de ficheros del host para Docker root (a menudo
/var/lib/docker) y para los montajes de logs. - Bytes de escritura por segundo y utilización de IO (await, señales tipo svctm según la herramienta).
- % libre de inodos. Puedes tener “50% libre” y aun así estar muerto porque te quedaste sin inodos.
- Crecimiento de imagenes Docker + capa writable de contenedores (si puedes rastrearlo). Si no, sigue la tasa de crecimiento de
/var/lib/docker.
Patrón de alerta que realmente funciona: alertar por tiempo-hasta-lleno (basado en tasa de crecimiento) en lugar de porcentaje crudo.
“El disco estará lleno en 6 horas” consigue acción. “El disco está 82%” se ignora hasta que está 99%.
4) CPU: uso alto vs throttling
- Uso de CPU por contenedor (núcleos o segundos/segundo).
- Tiempo throttled de CPU por contenedor y periodos throttled. Esta es la métrica de “nos quedamos sin CPU”.
- iowait del host. Si iowait sube con la latencia, tu cuello de botella es disco, no CPU.
Si el uso de CPU es moderado pero el throttling es alto, pusiste límites muy bajos o empaquetaste demasiados contenedores en el nodo. Los usuarios lo experimentan como
picos de latencia aleatorios. Los ingenieros lo diagnostican mal como “inestabilidad de red” porque los gráficos parecen estar bien.
5) Red: cuando “es DNS” es en realidad conntrack
- Retransmisiones TCP y errores de socket en el host.
- Uso de la tabla conntrack (% usado).
- Tasa de error DNS de tu resolvedor (SERVFAIL, timeout). DNS suele ser víctima, no la causa.
6) “Señales doradas” de la aplicación (las que merece la pena cablear)
- Tasa de requests, tasa de errores, latencia (p50/p95/p99), saturación (profundidad de colas, utilización de workers).
- Tasa de errores de dependencias (BD, cache, APIs upstream). Los incidentes Docker a menudo se manifiestan como tormentas en dependencias.
Alertas mínimas que no te harán odiar el pager
- Disk time-to-full < 12h (warn), < 2h (page)
- Inodes time-to-zero < 12h (warn), < 2h (page)
- OOM kill event (page)
- Bucle de reinicio: restarts/minute > baseline por 10m (page)
- Ratio de tiempo throttled CPU > 10% por 10m (warn), > 25% por 5m (page) para servicios sensibles a latencia
- Swap-in sostenido en host (warn), pico en tasa de faltas mayores de página (warn/page según)
- Conntrack utilization > 80% por 10m (warn), > 90% (page)
- p99 de app + tasa de errores ambos degradan (page). Uno sin el otro suele ser ruido.
Conjunto mínimo de logs (y cómo no ahogarse)
Las métricas te dicen algo anda mal. Los logs te dicen qué tipo de mal. La estrategia mínima de logging no es “loggear todo.”
Es “loggear los eventos que expliquen transiciones de estado y fallos, y mantenerlos el tiempo suficiente para depurar.”
stdout/stderr de contenedores: trátalo como una interfaz de producto
En Docker, stdout/stderr es la ruta de logs más conveniente y la más fácil de abusar. Si tu app loggea JSON, bien. Si loggea stack traces
y cuerpos completos de requests, también está bien—hasta que no lo está. Tu mínimo:
- Logs estructurados (JSON preferido) con timestamp, level, request ID y campos de error.
- Banner de arranque incluyendo versión, checksum de config y puertos de escucha.
- Resúmenes en una línea para fallos de requests y fallos de dependencias.
- Logs ruidosos con limitación de tasa (timeouts, reintentos). Los logs repetidos deben agregarse, no spamear.
Logs del daemon Docker y del host: lo aburrido que lo explica todo
- dockerd logs: pulls de imágenes, errores de graphdriver, fallos de exec, problemas de containerd.
- kernel logs: mensajes del OOM killer, errores de sistema de ficheros, drops de red.
- journald/syslog: reinicios de servicios, fallos de unidades, avisos de limitación de tasa.
Retención y rotación de logs: elige una política, no una esperanza
Si mantienes logs json-file sin rotación, eventualmente llenarás el disco. Esto no es un “tal vez”. Esto es física.
Chiste #1: Los logs Docker sin rotar son como un cajón de trastos: bien hasta que intentas cerrarlo y la cocina deja de funcionar.
Política mínima para json-file:
- Configurar max-size y max-file globalmente.
- Preferir envío externo de logs (journald, fluentd o un sidecar/agent) si necesitas retención más larga.
- Alerta por crecimiento de logs de la misma manera que alertas por crecimiento de disco. Los logs son solo escrituras de disco con opiniones.
Tareas prácticas: comandos, salidas, decisiones (12+)
Los dashboards son geniales hasta que no lo son. Cuando un host está en llamas, necesitas un pequeño conjunto de comandos que te digan qué falla:
CPU, memoria, disco, red, Docker en sí, o tu app. Abajo hay tareas que puedes ejecutar en cualquier host Docker.
Cada una incluye: comando, salida representativa, qué significa y qué decisión tomar.
Task 1: Confirmar el radio del impacto (qué está corriendo, qué se está reiniciando)
cr0x@server:~$ docker ps --format 'table {{.Names}}\t{{.Image}}\t{{.Status}}\t{{.Ports}}'
NAMES IMAGE STATUS PORTS
api-7c9d registry/app:1.8.2 Up 3 minutes (healthy) 0.0.0.0:8080->8080/tcp
worker-2a11 registry/worker:1.8.2 Restarting (1) 12 seconds ago
redis-01 redis:7 Up 14 days 6379/tcp
Qué significa: Un contenedor está en bucle de reinicio. “Restarting (1)” no es una vibra; es un síntoma.
Decisión: Triager la causa del reinicio inmediatamente (exit code, logs, OOM). No persigas latencia en otro lado hasta entender el bucle de reinicio.
Task 2: Ver por qué murió un contenedor (exit code + pista OOM)
cr0x@server:~$ docker inspect -f 'Name={{.Name}} ExitCode={{.State.ExitCode}} OOMKilled={{.State.OOMKilled}} Error={{.State.Error}} FinishedAt={{.State.FinishedAt}}' worker-2a11
Name=/worker-2a11 ExitCode=137 OOMKilled=true Error= FinishedAt=2026-01-03T00:12:41.981234567Z
Qué significa: Exit 137 con OOMKilled=true es un contenedor matado por el kernel/cgroup por violación del límite de memoria.
Decisión: Deja de adivinar “crashes aleatorios”. Aumenta el límite de memoria (con cuidado), reduce concurrencia o arregla la fuga. También verifica la presión de memoria del host.
Task 3: Recuperar los últimos logs sin ahogarse
cr0x@server:~$ docker logs --tail=80 --timestamps worker-2a11
2026-01-03T00:12:35.112Z level=info msg="starting worker" version="1.8.2" concurrency=64
2026-01-03T00:12:39.003Z level=error msg="failed processing job" err="context deadline exceeded" job_id=91311
2026-01-03T00:12:41.978Z level=info msg="shutdown requested"
Qué significa: Los logs muestran un arranque y algunos timeouts, pero nada sobre OOM (porque OOM está fuera del proceso).
Decisión: Correlaciona con kernel logs para OOM. No esperes a que la aplicación confiese.
Task 4: Confirmar OOM kills en el log del kernel
cr0x@server:~$ sudo dmesg -T | tail -n 30
[Sat Jan 3 00:12:41 2026] Memory cgroup out of memory: Killed process 28144 (worker) total-vm:2145932kB, anon-rss:612344kB, file-rss:1200kB, shmem-rss:0kB
[Sat Jan 3 00:12:41 2026] oom_reaper: reaped process 28144 (worker), now anon-rss:0kB, file-rss:0kB, shmem-rss:0kB
Qué significa: El kernel mató el proceso por límite de memoria de cgroup. Esto es concluyente.
Decisión: Trátalo como bug de capacidad/límite/config. Ajusta límites o arregla el comportamiento de memoria de la app; añade alerta en eventos OOMKill.
Task 5: Ver presión de memoria del host (el preludio de “todos van lentos”)
cr0x@server:~$ free -h
total used free shared buff/cache available
Mem: 31Gi 28Gi 620Mi 512Mi 2.4Gi 1.1Gi
Swap: 4.0Gi 1.9Gi 2.1Gi
Qué significa: La memoria disponible es baja y el swap está en uso. Si el swap cambia activamente, la latencia probablemente esté subiendo.
Decisión: Reduce la presión de memoria: mueve cargas, aumenta RAM del host, limita caches, ajusta límites. Considera el riesgo de paginación como incidente digno de atención.
Task 6: Identificar los mayores consumidores de memoria por contenedor
cr0x@server:~$ docker stats --no-stream --format 'table {{.Name}}\t{{.MemUsage}}\t{{.MemPerc}}\t{{.CPUPerc}}'
NAME MEMUSAGE / LIMIT MEMPERC CPUPERC
api-7c9d 420MiB / 1GiB 41.02% 85.31%
worker-2a11 980MiB / 1GiB 95.70% 12.01%
redis-01 210MiB / 2GiB 10.25% 1.10%
Qué significa: El worker está al límite de memoria. Volverá a OOMear.
Decisión: Reduce la concurrencia del worker, añade backpressure o sube el límite. Si subes el límite, verifica primero el margen del host.
Task 7: Comprobar el llenado de disco donde vive Docker
cr0x@server:~$ df -h /var/lib/docker
Filesystem Size Used Avail Use% Mounted on
/dev/nvme0n1p2 200G 189G 11G 95% /
Qué significa: El root de Docker está al 95%. Esto es “pronto a ser incidente”, no “está bien”.
Decisión: Inicia limpieza (imágenes no usadas, contenedores detenidos, build cache). También identifica la fuente de crecimiento (logs, overlay2, volúmenes).
Task 8: Encontrar agotamiento de inodos (el outage de disco sigiloso)
cr0x@server:~$ df -i /var/lib/docker
Filesystem Inodes IUsed IFree IUse% Mounted on
/dev/nvme0n1p2 13107200 13010000 97200 100% /
Qué significa: Puede haber espacio en disco, pero los inodos están prácticamente agotados. Crear archivos fallará; los contenedores pueden romperse de maneras extrañas.
Decisión: Identifica directorios con cuentas enormes de archivos (a menudo build cache, capas desempaquetadas o logs pequeños). Limpia y considera ajustar el filesystem.
Task 9: Medir qué se está comiendo el disco Docker (volúmenes vs capas vs logs)
cr0x@server:~$ sudo du -xhd1 /var/lib/docker | sort -h
1.2G /var/lib/docker/containers
6.8G /var/lib/docker/volumes
29G /var/lib/docker/buildkit
150G /var/lib/docker/overlay2
189G /var/lib/docker
Qué significa: overlay2 es enorme, el build cache es no trivial, y el directorio containers (logs) es pequeño aquí.
Decisión: Si overlay2 domina, busca capas writable grandes y proliferación de imágenes. Si buildkit es grande, prunealo. Si containers es grande, arregla la rotación de logs.
Task 10: Hacer prune de forma segura (y entender el radio del impacto)
cr0x@server:~$ docker system df
TYPE TOTAL ACTIVE SIZE RECLAIMABLE
Images 47 9 68.2GB 52.4GB (76%)
Containers 21 12 3.4GB 1.1GB (32%)
Local Volumes 18 11 6.8GB 1.9GB (27%)
Build Cache 163 0 29.0GB 29.0GB
Qué significa: Hay mucho espacio recuperable, especialmente build cache e imágenes antiguas.
Decisión: Usa pruning dirigido primero. Evita eliminar volúmenes a lo bestia a menos que estés seguro.
cr0x@server:~$ docker builder prune -f
Deleted build cache objects:
3yq9m3c2kz7qf2n2o6...
Total reclaimed space: 28.7GB
Qué significa: El build cache se limpió con éxito.
Decisión: Revisa el disco. Si sigue alto, prunea imágenes no usadas con cuidado.
Task 11: Comprobar tamaños de archivos de logs por contenedor (driver json-file)
cr0x@server:~$ sudo ls -lh /var/lib/docker/containers/*/*-json.log | sort -k5 -h | tail -n 5
-rw-r----- 1 root root 1.2G Jan 3 00:10 /var/lib/docker/containers/9c1.../9c1...-json.log
-rw-r----- 1 root root 2.8G Jan 3 00:11 /var/lib/docker/containers/aa4.../aa4...-json.log
-rw-r----- 1 root root 3.5G Jan 3 00:11 /var/lib/docker/containers/b7d.../b7d...-json.log
-rw-r----- 1 root root 6.1G Jan 3 00:12 /var/lib/docker/containers/cc9.../cc9...-json.log
-rw-r----- 1 root root 9.4G Jan 3 00:12 /var/lib/docker/containers/f01.../f01...-json.log
Qué significa: Uno o más contenedores están generando logs gigantes. Esto puede llenar disco y también ralentizar el host por IO.
Decisión: Arregla la verbosidad de logs y establece rotación. En emergencia, trunca el mayor culpable con cuidado (aceptando la pérdida).
Task 12: Verificar la configuración de rotación de logs de Docker (y aplicarla)
cr0x@server:~$ sudo cat /etc/docker/daemon.json
{
"log-driver": "json-file",
"log-opts": {
"max-size": "50m",
"max-file": "5"
}
}
Qué significa: json-file rotará a 50MB, manteniendo 5 archivos. Esto es lo mínimo para evitar la muerte por logs en disco.
Decisión: Si falta, añádelo y reinicia Docker en una ventana de mantenimiento. Si está pero sigue habiendo archivos gigantes, tus contenedores probablemente preexisten a la configuración; recréalos.
Task 13: Detectar CPU throttling (la pista de “límites demasiado estrictos”)
cr0x@server:~$ CID=$(docker ps -qf name=api-7c9d); docker inspect -f '{{.Id}}' $CID
b3e2f0a1c9f7f4c2b3a0d0c2d1e9a8b7c6d5e4f3a2b1c0d9e8f7a6b5c4d3e2f
cr0x@server:~$ cat /sys/fs/cgroup/cpu/docker/$CID/cpu.stat
nr_periods 124030
nr_throttled 38112
throttled_time 921837239112
Qué significa: Alto nr_throttled y gran throttled_time indican que el contenedor frecuentemente pidió CPU pero fue throttled.
Decisión: Sube el límite de CPU, reduce la cantidad de contenedores por nodo o optimiza hotspots de CPU. Si los picos de latencia correlacionan, el throttling es la pista principal.
Task 14: Comprobar presión de IO del host (cuando “todo se pone lento”)
cr0x@server:~$ iostat -xz 1 3
Linux 6.1.0 (server) 01/03/2026 _x86_64_ (16 CPU)
avg-cpu: %user %nice %system %iowait %steal %idle
18.21 0.00 6.02 21.33 0.00 54.44
Device r/s w/s rkB/s wkB/s await aqu-sz %util
nvme0n1 85.1 410.2 8120.0 65211.1 18.4 2.31 94.7
Qué significa: Alto iowait y %util cerca de saturación. El disco es el cuello de botella; la CPU espera por IO.
Decisión: Reduce la amplificación de escritura (logs, picos de flush de BD), mueve cargas o escala IO. No “añadas CPU”; no ayudará.
Task 15: Comprobar saturación de conntrack (el generador de timeouts de red aleatorios)
cr0x@server:~$ sudo sysctl net.netfilter.nf_conntrack_count net.netfilter.nf_conntrack_max
net.netfilter.nf_conntrack_count = 245112
net.netfilter.nf_conntrack_max = 262144
Qué significa: Conntrack está casi lleno. Nuevas conexiones pueden fallar de formas feas e intermitentes.
Decisión: Aumenta conntrack max (siempre con cuidado de memoria), reduce churn de conexiones, ajusta timeouts y revisa tormentas de reintentos.
Chiste #2: Conntrack es donde las conexiones van a ser recordadas para siempre, lo cual es romántico hasta que tu nodo se queda sin memoria.
Playbook de diagnóstico rápido: primeras/segundas/terceras comprobaciones
Cuando la latencia sube o los errores se disparan, necesitas responder una pregunta rápido: qué recurso está saturado o fallando primero?
Aquí hay un playbook que funciona incluso cuando tu monitoreo está retrasado, faltante o mintiendo.
Primera comprobación: ¿es un crash loop, OOM o regresión de deploy?
- docker ps: busca Restarting, unhealthy o contenedores iniciados recientemente.
- docker inspect: códigos de salida, bandera OOMKilled, estado de health.
- docker logs –tail: últimas 50–200 líneas para errores fatales y config de arranque.
Decisión rápida: Si ves reinicios creciendo tras un cambio, congela deploys y haz rollback. La depuración viene después de estabilidad.
Segunda comprobación: presión de disco e inodos (porque rompe todo)
- df -h en
/var/lib/dockery montajes de logs. - df -i para agotamiento de inodos.
- du para encontrar qué subdirectorio de Docker está creciendo.
Decisión rápida: Si disco > 90% y creciendo, empieza a recuperar espacio inmediatamente. Esta es una de las pocas veces en que “limpieza” es respuesta válida de incidentes.
Tercera comprobación: saturación de recursos del host (memoria, IO, CPU throttling)
- free -h y actividad de swap: confirma presión de memoria.
- iostat -xz: muestra saturación de IO y iowait.
- cpu.stat throttling: confirma límites de CPU demasiado estrictos.
Decisión rápida: La saturación significa que debes descargar carga, mover contenedores o cambiar límites—depurar la app no arreglará la física.
Cuarta comprobación: agotamiento de tablas de red y síntomas DNS
- conteo/max de conntrack: cerca de lleno causa timeouts.
- ss -s (no mostrado arriba): estados de sockets; muchos TIME-WAIT pueden indicar churn.
- logs de la app: timeouts a dependencias; correlaciona con conntrack y retransmisiones.
Quinta comprobación: salud del daemon Docker
- dockerd logs: errores del driver de almacenamiento, pulls fallidos, mensajes de “no space left”.
- docker info: confirma storage driver, cgroup driver, root dir.
Una cita, porque sigue siendo el mejor encuadre para trabajo de incidentes:
La esperanza no es una estrategia.
— James Cameron
Tres mini-historias del mundo corporativo
Mini-historia 1: El incidente causado por una suposición equivocada
Una empresa mediana operaba un conjunto de hosts Docker para APIs internas. El equipo supuso que los contenedores estaban “suficientemente aislados” y trató el host como un sustrato tonto.
Tenían alertas para HTTP 5xx y CPU básica. ¿Disco? Lo revisaban “a veces”.
Un viernes se desplegó una nueva función con logs de debug verbosos activados por accidente. El servicio siguió en pie; las tasas de error eran normales. Pero los logs crecieron rápido,
y a la madrugada el filesystem root de Docker estaba casi lleno. El primer síntoma no fue “disco lleno”. Fue despliegues más lentos, porque los pulls de imagen
y las extracciones de capas empezaron a thrash. Luego los clientes de la BD comenzaron a hacer timeouts porque el IO del nodo estaba saturado.
El equipo pasó horas discutiendo sobre “inestabilidad de red” porque los pings estaban bien y la CPU no estaba al máximo. Finalmente alguien ejecutó df -h y vio 99% usado.
Truncaron un gran log json y vieron la recuperación inmediata del servicio.
Post-mortem, la suposición errónea fue obvia: “Si la app está healthy, el host está bien.” Docker no evita que el disco se llene; solo te da más
sitios donde ocultar uso de disco. La solución también fue obvia: alertas por crecimiento de disco y rotación de logs por defecto, aplicadas vía gestión de configuración.
Mini-historia 2: La optimización que salió mal
Otra organización quiso mejor bin-packing y reducir costos de cloud. Ajustaron límites de CPU para varios servicios sensibles a latencia,
argumentando que el uso promedio de CPU era bajo. Consiguieron el ahorro. Luego llegaron los gráficos.
Tras el cambio, clientes reportaron “lentitud esporádica”. Los servicios no caían. Las tasas de error eran modestas. El on-call veía uso de CPU
rondando 40–50% y declaró el nodo sano. Mientras tanto el p99 de latencia se duplicó durante tráfico pico y luego “misteriosamente” volvió a la normalidad.
El verdadero culpable fue el CPU throttling. Workloads con ráfagas chocaban con sus límites y eran throttled justo cuando necesitaban ráfagas cortas para mantener colas vacías.
El SO hizo exactamente lo que se le indicó. El equipo no tenía métricas de throttling, así que parecía un fantasma.
La optimización que salió mal no fue “límites mal puestos”. Fue “límites sin visibilidad de throttling es malo”. Añadieron alertas de tiempo throttled,
incrementaron límites de CPU para algunos servicios y ajustaron concurrencia para que reflejara lo que realmente podían usar los contenedores.
Mini-historia 3: La práctica aburrida pero correcta que salvó el día
Una plataforma financiera tenía hosts Docker con una base estricta: rotación de logs configurada en /etc/docker/daemon.json, pruning nocturno del build cache,
y alertas por tiempo-a-llenarse en /var/lib/docker. Nadie se jactaba. Era simplemente “la regla”.
Durante una corrida de batch de fin de trimestre, un worker empezó a producir muchos más logs de lo usual por un timeout en una dependencia upstream. El servicio fue ruidoso,
pero no tumbó el host. Los logs rotaron. El uso de disco subió, pero la alerta de tiempo-a-llenarse saltó con suficiente antelación para investigar sin pánico.
El equipo rastreó el problema real hasta una dependencia y arregló la tormenta de reintentos. Lo importante es lo que no pasó: el host Docker no llegó al 100% de disco,
los contenedores no dejaron de escribir ficheros temporales y el incidente no se convirtió en un outage en cascada.
“Aburrido pero correcto” no da pie a postmortems emocionantes. Sí reduce la cantidad de postmortems.
Errores comunes: síntoma → causa raíz → solución
1) Síntoma: Los contenedores se reinician cada pocos minutos, sin error evidente de la app
Causa raíz: OOM kills por límite de memoria demasiado bajo o fuga de memoria; los logs de la app no lo muestran.
Solución: Confirma con docker inspect OOMKilled y el kernel con dmesg. Aumenta el límite o reduce concurrencia; añade alertas de OOM y rastrea el working set.
2) Síntoma: “No space left on device” pero df muestra espacio libre
Causa raíz: Agotamiento de inodos o bloques reservados por filesystem; a veces presión de metadata de overlay2.
Solución: Revisa con df -i. Elimina directorios con gran cantidad de archivos (build cache, temporales), prunea artefactos Docker, ajusta ratio de inodos si procede.
3) Síntoma: Picos de latencia aleatorios, CPU parece estar bien
Causa raíz: CPU throttling por límites estrictos; el contenedor quiere CPU pero se lo niegan.
Solución: Monitorea tiempo throttled. Sube el límite de CPU o reduce concurrencia; deja de usar solo “CPU%” como señal de capacidad.
4) Síntoma: Deploys cuelgan o pulls de imágenes son lentos, luego los servicios degradan
Causa raíz: Saturación de IO de disco o thrash del storage driver; a menudo combinado con disco casi lleno.
Solución: Usa iostat y comprobaciones de espacio. Reduce IO (logs, rebuild storms), añade margen de disco y evita builds pesados en nodos de producción.
5) Síntoma: Timeouts de red en muchos contenedores, especialmente salientes
Causa raíz: Tabla conntrack casi llena; tráfico NAT con churn elevado.
Solución: Revisa conteo/max de conntrack; ajusta nf_conntrack_max y timeouts; reduce churn de conexiones (keepalives, pooling) y evita tormentas de reintentos.
6) Síntoma: Los logs de un contenedor son gigantes; el disco del nodo sigue llenándose
Causa raíz: Logs json-file sin rotación; verbosidad excesiva; o bucles de error repetidos.
Solución: Configura rotación de logs Docker; reduce volumen de logs; añade limitación de tasa; envía logs fuera del host si se requiere retención.
7) Síntoma: Contenedor “Up” pero el servicio está muerto o devuelve errores
Causa raíz: Sin healthcheck, o un healthcheck demasiado débil (chequea proceso, no funcionalidad).
Solución: Añade HEALTHCHECK que valide conectividad a dependencias o un endpoint real de readiness; alerta sobre estado unhealthy antes de reinicios.
8) Síntoma: Uso de disco crece pero pruning no ayuda mucho
Causa raíz: Volúmenes acumulando datos, o la app escribiendo en la capa writable del contenedor; no son solo imágenes no usadas.
Solución: Identifica con docker system df y du en filesystem. Mueve escrituras a volúmenes con gestión de ciclo de vida; implementa políticas de retención.
Listas de verificación / plan paso a paso
Plan de implementación de observabilidad mínima (una semana, realista)
- Elige las alertas mínimas (tiempo-a-llenarse disco, tiempo-a-cero inodos, OOM kills, bucles de reinicio, CPU throttling, saturación conntrack, latencia+errores app).
- Estandariza logging Docker: establece
max-size/max-fileen/etc/docker/daemon.json; aplica vía config management. - Etiqueta servicios sensatamente: métricas etiquetadas por servicio y entorno, no por ID de contenedor. Mantén el ID de contenedor para depuración, no para alertas.
- Expone las señales doradas de la app: tasa de requests, tasa de errores, percentiles de latencia, saturación. Si solo puedes hacer una, haz tasa de error + p99.
- Recolecta métricas del host para disco, inodos, IO, memoria, swap, conntrack. Sin métricas de host, las métricas de contenedor te gaslightearán.
- Recolecta métricas de contenedores (uso CPU, throttling, working set de memoria, reinicios). Valida comparando con
docker statsdurante carga. - Escribe una página de runbook por alerta: qué comprobar, comandos a ejecutar y pasos de rollback/mitigación.
- Realiza un game day: simula llenado de disco (en entorno seguro), simula un OOM, simula presión de conntrack. Valida alertas y playbook.
Checklist de seguridad de disco (porque el disco te traicionará)
- Rotación de logs Docker configurada y verificada en contenedores nuevos
- Alerta por tasa de crecimiento/tiempo-a-llenarse para Docker root
- Alerta por consumo de inodos/tasa de tiempo-a-cero
- Pruning nocturno del build cache para nodos de build (no necesariamente prod)
- Retención explícita para volúmenes (las bases de datos y colas no son basureros)
Checklist de seguridad CPU/memoria
- Alerta por OOM kills y working set de memoria acercándose a límites
- Alerta por CPU throttling, no solo por uso de CPU
- Límites definidos con pruebas de carga reales; concurrencia atada a presupuesto de CPU/memoria
- Swap del host monitorizado; swap-in sostenido es una señal de advertencia que debes respetar
Checklist de logging (sanidad mínima viable)
- Logs estructurados con request IDs y campos de error
- Errores repetitivos con limitación de tasa
- No incluir cuerpos de request/response por defecto en producción
- Separar logs de auditoría de logs de debug si se requiere
FAQ
1) ¿Cuál es la alerta más importante para Docker?
Tiempo-a-llenarse del disco (y tiempo-a-cero de inodos como su igualmente molesto hermano). Los outages por disco causan fallos en cascada y corrompen la línea de tiempo del incidente.
Si solo añades una cosa, que sea “estaremos llenos en X horas”.
2) ¿Debería alertar por reinicios de contenedores?
Sí, pero alerta por tasa de reinicios por servicio, no por cada reinicio aislado. Un despliegue en rolling causa reinicios; un crash loop causa una tasa sostenida.
Incluye también códigos de salida o señales OOMKilled en el contexto de la alerta.
3) ¿Por qué mis logs de app no muestran OOM kills?
Porque el kernel mata el proceso. Tu app no tiene oportunidad de loggear un bonito mensaje de despedida. Confirma con docker inspect y dmesg.
4) ¿Es json-file aceptable en producción?
Está bien si configuras rotación y entiendes que los logs viven en el filesystem del host. Si necesitas retención más larga, envía los logs a otro sitio.
json-file sin rotar es una máquina de llenar disco con apariencia de negación plausible.
5) CPU está solo al 40%, ¿por qué la latencia es terrible?
Revisa CPU throttling e iowait. “CPU 40%” puede significar que te están negando la mitad de tu cuota de CPU, o que la CPU está idle porque espera al disco.
Los gráficos de uso no muestran la negación; el throttling sí.
6) ¿Cómo distingo problemas de espacio en disco de problemas de IO de disco?
Los problemas de espacio aparecen en df -h y errores “no space left”. Los problemas de IO aparecen como iowait alto y alta utilización del dispositivo en iostat.
A menudo van juntos, pero la saturación de IO puede ocurrir con bastante espacio libre.
7) ¿Por qué “docker system prune” no libera mucho espacio?
Porque tu uso de disco puede estar en volúmenes, capas writables de contenedores o imágenes activas. docker system df te dice qué es recuperable.
Si los volúmenes son el problema, prune no los tocará a menos que los elimines explícitamente (lo cual puede ser catastrófico).
8) ¿Necesito tracing distribuido completo para la observabilidad mínima de Docker?
No para el mínimo. El tracing es excelente para rutas de requests complejas, pero no te salvará de discos llenos, OOM kills o agotamiento de conntrack.
Consigue primero las señales aburridas de host/contenedor. Luego añade traces donde realmente paguen por sí mismos.
9) ¿Cuál es una configuración razonable de rotación de logs?
Un baseline común es max-size=50m y max-file=5 para servicios generales. Servicios de alto volumen pueden necesitar tamaños menores o drivers distintos.
La configuración “correcta” es la que evita la agonía por disco y aún deja historial suficiente para depurar.
Conclusión: próximos pasos que realmente mueven la aguja
Las fallas de Docker rara vez son misteriosas. Suelen ser presión de recursos, límites mal configurados, logs desbocados o agotamiento de tablas de red—más una capa delgada de negación humana.
El conjunto mínimo de observabilidad es cómo cortas la negación rápidamente.
Pasos prácticos siguientes:
- Implementar rotación de logs Docker globalmente y verificar que los contenedores nuevos la hereden.
- Añadir alertas por tiempo-a-llenarse y tiempo-a-cero de inodos en el filesystem root de Docker.
- Alerta por OOM kills y tasas de bucle de reinicio por servicio.
- Comenzar a monitorear CPU throttling; dejar de fingir que solo CPU% cuenta la historia.
- Añadir visibilidad de saturación de conntrack si ejecutas workloads NAT-heavy o con muchas conexiones.
- Escribir un runbook de una página que incluya los comandos exactos que ejecutarás bajo presión (usa las tareas anteriores).
Si solo haces eso, detectarás la mayoría de los outages Docker temprano. Y pasarás menos tiempo mirando dashboards que parecen tranquilos mientras producción arde en silencio.