Hay un tipo de incidencia en la que nada está “caído” porque todo está constantemente “arrancando”. Tus paneles muestran picos de CPU, el volumen de logs se dispara y el nombre del contenedor es un borrón de reinicios. Intentas usar docker exec, pero el proceso muere antes de que llegue el prompt del shell. Felicidades: has creado un bucle infinito de fallos.
Las políticas de reinicio de Docker pretenden hacer que los servicios sean resistentes. En producción, también pueden convertir una pequeña falla en un incidente autosostenido: ruidoso, caro y difícil de depurar. Así es como dejas de hacerlo.
Políticas de reinicio: lo que realmente hacen (no lo que esperas)
Las políticas de reinicio de Docker son simples sobre el papel. En la práctica, son un contrato entre el ciclo de vida de tu contenedor y el comportamiento opinionado del daemon. No hacen que tu app sea más saludable. La hacen persistente. Esas son propiedades diferentes, y confundirlas es cómo obtienes bucles infinitos de fallos con “autorreparación” escrito en el postmortem.
Las cuatro políticas que realmente usas
- no (por defecto): Docker no reiniciará el contenedor cuando termine. Esto no es “inseguro”. A menudo es la opción más sensata para trabajos por lotes y tareas puntuales.
- on-failure[:max-retries]: Reinicia sólo si el contenedor sale con un código distinto de cero. Opcionalmente se detiene después de N reintentos. Esto es lo más parecido que tiene Docker a “intenta un poco, luego para de hacer el tonto”.
- always: Reinicia independientemente del código de salida. Si el daemon se reinicia, el contenedor también vuelve. Esta es la política que convierte un “apagado ordenado” en una “resurrección sorpresa”.
- unless-stopped: Como
always, excepto que un stop manual sobrevive a reinicios del daemon. Es “always, pero con respeto por la intervención humana”.
Qué Docker considera un “reinicio” (y por qué importa)
Docker reinicia un contenedor cuando el proceso principal del contenedor sale. Eso es PID 1 dentro del contenedor. Si tu PID 1 es un script de shell que hace fork al servicio real y luego sale, Docker interpretará eso como “el servicio murió” y lo reiniciará… para siempre. La política de reinicio no está rota; tu estrategia de init lo está.
Además: Docker tiene un retraso/retroceso de reinicio incorporado. No es un interruptor configurable tipo circuit breaker en el Docker Engine clásico. Evita un bucle estrecho de reinicios por segundo, pero no detendrá un bucle persistente. Sólo hace que tu incidente dure más y sea más confuso.
Una cita, porque sigue siendo cierta en 2026: «La esperanza no es una estrategia.»
— General Gordon R. Sullivan.
Cómo elegir una política en producción (versión opinionada)
Si ejecutas servicios de producción en hosts individuales (o pequeñas flotas) con Docker Engine o Compose, trata las políticas de reinicio como una salvaguarda de último tramo, no como tu mecanismo principal de fiabilidad.
- Usa
on-failure:5por defecto para la mayoría de servicios de larga duración que esperas que fallen rara vez. Si no puede arrancar tras 5 intentos, algo está mal. Para y alerta. - Usa
unless-stoppedcuando tengas una razón sólida (p. ej., sidecars de infra simples, desarrollo local, o un host que debe arrancar limpio tras un reinicio). Aun así: instruméntalo. - Evita
alwayspara cualquier cosa que pueda fallar rápido (config mala, secreto faltante, mismatch de esquema, migraciones). “Always” es cómo quemas CPU sin hacer nada útil. - Usa
nopara trabajos por lotes. Si tu job nocturno falla, probablemente quieres que falle en voz alta, no que se reejecute eternamente y mande 400 correos a finanzas.
Primer chiste corto: los contenedores no se curan solos; sólo se vuelven muy buenos en la reencarnación.
Hechos e historia que cambian tu forma de pensar sobre los reinicios
Un poco de contexto hace que el comportamiento de reinicio de Docker parezca menos arbitrario y más una serie de trade-offs que se filtraron a tu pager.
- Las políticas de reinicio de Docker preceden la adopción masiva de Kubernetes, cuando la gestión de contenedores en un solo host era el caso común y “mantenerlo en ejecución” era la petición principal.
- El “problema PID 1” es historia antigua de Unix: señales, zombies y recolección de procesos. Los contenedores no lo inventaron; lo hicieron imposible de ignorar.
- La semántica de los códigos de salida es un contrato: Docker los usa para
on-failure. Si tu app sale con 0 en un error (“todo bien!”) has elegido el caos. - Los bucles de reinicio existían mucho antes de los contenedores: unidades systemd con
Restart=alwayspueden hacer el mismo daño. Docker sólo lo hizo fácil desde una one-liner. - Los healthchecks llegaron más tarde de lo que mucha gente asume. Durante mucho tiempo, “contenedor arriba” significaba “el proceso existe”, no “el servicio funciona”. Ese legado sigue marcando patrones comunes.
- Los drivers de logs importan históricamente: el driver por defecto
json-filehacía fácil llenar discos durante reinicios. Eso no es teórico; es un reincidente. - El comportamiento de OOM-kill es una realidad del kernel: el contenedor no “se cayó”, el kernel lo mató. Docker reporta el síntoma; aún tienes que leer la autopsia.
- El retroceso de Docker no es un circuito cerrador completo. Ralentiza la frecuencia de reinicios, pero no decide detenerse. Esa decisión es tuya mediante políticas y automatización.
Por qué ocurren los bucles de fallo: modos de fallo, no fallas morales
Un bucle de fallo suele ser uno de estos:
- Configuración incorrecta o dependencia faltante: variable de entorno errónea, archivo faltante, DNS malo, DB inaccesible, secreto no montado.
- La app sale intencionalmente: migraciones requeridas, verificación de licencia fallida, feature flag inválido, imagen “run once” usada como servicio.
- Presión de recursos: OOM kills, throttling de CPU causando timeouts, disco lleno, agotamiento de inodos, límites de descriptores de archivo.
- Orden de arranque roto: la app arranca antes que DB/cola esté lista; sin lógica de reintento sale inmediatamente.
- PID 1 deficiente: scripts de shell que salen temprano; sin init; señales no manejadas; procesos zombie que se acumulan y luego colapsan.
- Estado corrupto: volúmenes con actualizaciones parciales, archivos de bloqueo o versiones de esquema que no coinciden con el binario.
- Troteo externo: límite de tasa por upstream; la app lo trata como fatal y sale; los reinicios amplifican la estampida.
Segundo chiste corto: “Pusimos restart: always por fiabilidad” es el equivalente contenedor de poner cinta sobre la luz del motor.
Guion de diagnóstico rápido
Cuando estás en el incidente y el contenedor está flapeando, no tienes tiempo para filosofía. Aquí está el orden que encuentra el cuello de botella rápido.
Primero: establece si esto es fallo de la app o fallo de la plataforma
- Comprueba el recuento de reinicios y el último código de salida. Si ves códigos 1/2/78 o similares, probablemente es app/config. Si ves 137, piensa en OOM/kill. Si ves 0 con reinicios, tu política es
alwayso el daemon se reinició. - Mira las últimas 50 líneas de log del intento anterior. Buscas un error explícito, no “starting…” repetido hasta el infinito.
- Revisa dmesg/journal del host por OOM o errores de disco. Los contenedores no te pueden decir que el kernel los mató a menos que preguntes al kernel.
Segundo: detener la hemorragia (sin perder evidencia)
- Desactiva los reinicios temporalmente para que puedas inspeccionar el estado y los logs. No borres el contenedor a menos que ya hayas capturado lo que necesitas.
- Haz snapshot de la configuración e inspecciona los mounts. La mayoría de los “bucles misteriosos” son un “ruta de archivo equivocada” con pasos extra.
Tercero: decide si arreglas la app, el host o la política
- Si es config/dependencia, arregla la configuración o implementa reintentos/retroceso en la app. La política de reinicio no es un algoritmo de reintento.
- Si es presión de recursos, establece límites de memoria apropiados, ajusta el logging, aumenta disco o mueve cargas. Las políticas de reinicio no crean RAM.
- Si es política mala, cambia a
on-failure:5ounless-stoppedcon alertas en reinicios.
Tareas prácticas (comandos + salida + decisiones)
Querías comandos. Aquí tienes comandos. Cada tarea incluye: qué ejecutar, qué significa la salida y qué decisión tomar después.
Task 1: Identify the flapping container fast
cr0x@server:~$ docker ps --format 'table {{.Names}}\t{{.Image}}\t{{.Status}}\t{{.RunningFor}}'
NAMES IMAGE STATUS RUNNING FOR
api myco/api:1.42.0 Restarting (1) 6 seconds ago 2 minutes ago
postgres postgres:16 Up 3 hours (healthy) 3 hours ago
nginx nginx:1.25 Up 3 hours 3 hours ago
Significado: api se está reiniciando; Docker muestra el último código de salida entre paréntesis. Está fallando rápidamente (cada pocos segundos).
Decisión: Centrarte en api. No toques las dependencias saludables todavía.
Task 2: Inspect restart policy and restart count
cr0x@server:~$ docker inspect -f 'Name={{.Name}} Policy={{.HostConfig.RestartPolicy.Name}} Max={{.HostConfig.RestartPolicy.MaximumRetryCount}} Restarts={{.RestartCount}}' api
Name=/api Policy=always Max=0 Restarts=37
Significado: La política es always; el máximo de reintentos es ilimitado; ya hay 37 reinicios.
Decisión: Estás en un “bucle infinito por diseño”. Planea desactivar los reinicios para depurar.
Task 3: Get the last exit code and termination reason
cr0x@server:~$ docker inspect -f 'ExitCode={{.State.ExitCode}} OOMKilled={{.State.OOMKilled}} Error={{.State.Error}} FinishedAt={{.State.FinishedAt}}' api
ExitCode=1 OOMKilled=false Error= FinishedAt=2026-01-03T09:14:22.948601234Z
Significado: Código de salida 1, no OOM, sin error en tiempo de ejecución. Probablemente lógica de app/config/dependencia.
Decisión: Leer logs del intento de inicio que falla.
Task 4: Pull logs from the previous run (not just the current flicker)
cr0x@server:~$ docker logs --tail 80 api
2026-01-03T09:14:21Z INFO starting api version=1.42.0
2026-01-03T09:14:22Z ERROR config: missing required env var: DATABASE_URL
2026-01-03T09:14:22Z INFO exiting with code=1
Significado: El contenedor se comporta correctamente: se niega a arrancar sin configuración.
Decisión: Arreglar la inyección de entorno/secreto. Además: esto es exactamente por qué always es un mal predeterminado—este error nunca se autocurará.
Task 5: Confirm the container’s environment (what Docker thinks it is)
cr0x@server:~$ docker inspect -f '{{range .Config.Env}}{{println .}}{{end}}' api | sed -n '1,12p'
NODE_ENV=production
LOG_LEVEL=info
PORT=8080
Significado: No hay DATABASE_URL. Esto no es un problema de DNS en tiempo de ejecución; es falta de configuración al crear el contenedor.
Decisión: Actualizar Compose, unidad systemd o la herramienta de despliegue para proporcionar la variable (o montar un archivo y apuntar a él).
Task 6: Stop the restart loop without deleting the container
cr0x@server:~$ docker update --restart=no api
api
Significado: Política de reinicio actualizada. Docker no la reiniciará después del próximo stop/exit.
Decisión: Pararlo limpiamente, luego arreglar la configuración y arrancarlo una vez con ajustes comprobados.
Task 7: Stop it now (so you can inspect state and host pressure)
cr0x@server:~$ docker stop -t 10 api
api
Significado: El contenedor está parado y debería permanecer así porque estableciste restart=no.
Decisión: Si vuelve a arrancar, algo más (systemd, watchdog) lo está resucitando.
Task 8: Check whether systemd is restarting Docker itself (daemon restarts can look like container restarts)
cr0x@server:~$ systemctl status docker --no-pager
● docker.service - Docker Application Container Engine
Loaded: loaded (/lib/systemd/system/docker.service; enabled; vendor preset: enabled)
Active: active (running) since Sat 2026-01-03 09:00:05 UTC; 20min ago
TriggeredBy: ● docker.socket
Docs: man:docker(1)
Main PID: 1123 (dockerd)
Tasks: 23
Memory: 312.4M
CPU: 1min 54.931s
Significado: El daemon Docker está estable ahora mismo.
Decisión: Trata esto como un problema de contenedor/app, no como un daemon inestable.
Task 9: Check for OOM kills at the host level (even when Docker says OOMKilled=false)
cr0x@server:~$ sudo journalctl -k --since "10 min ago" | tail -n 12
Jan 03 09:12:01 server kernel: Memory cgroup out of memory: Killed process 24081 (node) total-vm:1820040kB, anon-rss:612340kB, file-rss:2140kB, shmem-rss:0kB
Jan 03 09:12:01 server kernel: oom_reaper: reaped process 24081 (node), now anon-rss:0kB, file-rss:0kB, shmem-rss:0kB
Significado: El kernel mató un proceso en un cgroup de memoria. Dependiendo del timing y de cómo murió el contenedor, las banderas de estado de Docker no siempre cuentan la historia completa.
Decisión: Si coincide con el PID de tu contenedor, necesitas límites de memoria y/o arreglar la app, no más reinicios.
Task 10: Confirm memory limits and whether they’re sane
cr0x@server:~$ docker inspect -f 'MemLimit={{.HostConfig.Memory}} MemSwap={{.HostConfig.MemorySwap}} PidsLimit={{.HostConfig.PidsLimit}}' api
MemLimit=268435456 MemSwap=268435456 PidsLimit=0
Significado: Límite de 256 MiB sin swap adicional. Esto está bien para un servicio Go pequeño; es una trampa para una app Node con heaps grandes.
Decisión: O subes el límite o configuras el heap en runtime; luego cambia a on-failure:5 para que una regresión no se convierta en un DoS contra tu propio host.
Task 11: Check disk pressure and log growth (restart loops love filling disks)
cr0x@server:~$ df -h /var/lib/docker
Filesystem Size Used Avail Use% Mounted on
/dev/nvme0n1p3 200G 189G 1.2G 100% /var/lib/docker
Significado: La ruta de almacenamiento de Docker está llena. Esto puede causar fallos secundarios extraños (pulls de imágenes fallan, escrituras de contenedores fallan, riesgo de corrupción de metadatos).
Decisión: Para los contenedores que flapean, limpia con seguridad y limita logs. No sigas reiniciando sobre un disco lleno.
Task 12: Identify which containers are producing huge JSON logs
cr0x@server:~$ sudo du -h /var/lib/docker/containers/*/*-json.log 2>/dev/null | sort -h | tail -n 5
2.1G /var/lib/docker/containers/8f2c.../8f2c...-json.log
3.8G /var/lib/docker/containers/31ab.../31ab...-json.log
5.4G /var/lib/docker/containers/aa90.../aa90...-json.log
6.0G /var/lib/docker/containers/3c11.../3c11...-json.log
7.2G /var/lib/docker/containers/1d77.../1d77...-json.log
Significado: Algunos contenedores escriben logs de varios gigabytes. Los bucles de reinicio multiplican esto rápidamente porque cada arranque registra los mismos banners y stack traces.
Decisión: Habilita rotación de logs en la configuración del daemon y arregla la app ruidosa. Mientras tanto, libera espacio con cuidado.
Task 13: Verify the container’s last start attempt timestamps
cr0x@server:~$ docker inspect -f 'StartedAt={{.State.StartedAt}} FinishedAt={{.State.FinishedAt}}' api
StartedAt=2026-01-03T09:14:21.115312345Z FinishedAt=2026-01-03T09:14:22.948601234Z
Significado: Vive ~1.8 segundos. Eso no es un error “transitorio”; es un fallo determinista de arranque.
Decisión: Deja de usar always. Arregla la configuración, luego arranca una vez y vuelve a habilitar una política de reinicio limitada.
Task 14: Get the exact command/entrypoint Docker is running
cr0x@server:~$ docker inspect -f 'Entrypoint={{json .Config.Entrypoint}} Cmd={{json .Config.Cmd}}' api
Entrypoint=["/bin/sh","-c"] Cmd=["/app/start.sh"]
Significado: PID 1 es /bin/sh -c, que ejecuta un script. Esta es una fuente clásica de problemas con el manejo de señales y de “script que sale temprano”.
Decisión: Inspecciona el script. Prefiere entrypoints en forma exec y añade un init si es necesario.
Task 15: Reproduce the failure interactively (without the restart policy)
cr0x@server:~$ docker run --rm -it --entrypoint /bin/sh myco/api:1.42.0 -lc '/app/start.sh; echo exit=$?'
config: missing required env var: DATABASE_URL
exit=1
Significado: Reprodujiste el problema fuera del contenedor flapeante. Eso es progreso: es determinista.
Decisión: Arreglar la inyección de entorno, no Docker.
Task 16: Apply a sane policy after fixing config
cr0x@server:~$ docker update --restart=on-failure:5 api
api
Significado: Si falla repetidamente, se detiene tras cinco fallos.
Decisión: Combina esto con alertas sobre el recuento de reinicios para que “detenido tras cinco” sea un pager, no un tiempo de inactividad silencioso.
Task 17: Validate healthcheck behavior (healthchecks don’t restart containers by themselves)
cr0x@server:~$ docker inspect -f 'Health={{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}' api
Health=unhealthy
Significado: Docker lo marca como unhealthy, pero no reiniciará automáticamente sólo porque esté unhealthy (comportamiento clásico del Docker Engine).
Decisión: Si necesitas “unhealthy que dispare reinicio”, necesitas un controlador externo (o un orquestador diferente), o implementar auto-terminación en la app ante una falla de salud irreparable (con precaución).
Task 18: Watch restart events in real time
cr0x@server:~$ docker events --since 5m --filter container=api
2026-01-03T09:10:21.115Z container start 8b4f... (name=api)
2026-01-03T09:10:22.948Z container die 8b4f... (exitCode=1, name=api)
2026-01-03T09:10:24.013Z container start 8b4f... (name=api)
Significado: Puedes ver la cadencia del bucle y los códigos de salida. Útil cuando los logs están ruidosos o rotados.
Decisión: Si los reinicios correlacionan con eventos del host (reinicio del daemon, fallo de red), amplía el alcance. Si la cadencia es estable, es app/config.
Healthchecks, readiness de dependencias y el mito de “reiniciar lo arregla”
La mayoría de los bucles de reinicio son problemas de readiness de dependencias que se hacen pasar por “rareza de Docker”. Tu app arranca, intenta la base de datos una vez, falla, sale. Docker la reinicia. Repite hasta la muerte térmica del universo o hasta que la DB responda y tengas suerte.
Haz esto en la app: reintentos con backoff y distinguir fatal vs transitorio
Si la base de datos está caída 30 segundos durante mantenimiento, salir inmediatamente no es “limpio”. Es frágil. Implementa reintentos de conexión con backoff exponencial y un presupuesto de tiempo máximo. Si el error es fatal (contraseña mala, host equivocado), sal una vez y deja que los reintentos limitados on-failure cubran un problema de orden de despliegue transitorio.
Haz esto en Docker: usa healthchecks para observabilidad y gating, no para curas mágicas
Los healthchecks son valiosos porque te dan una señal legible por máquinas: healthy/unhealthy. En Docker clásico, no reinician automáticamente el contenedor, pero:
- te ayudan a ver “el proceso está corriendo pero el servicio está muerto”,
- se integran con condiciones
depends_onen Compose (en implementaciones más nuevas), - dan a tu monitorización externa algo mejor que “el contenedor existe”.
Checks de dependencias: evita scripts “wait-for-it” que nunca terminan
Hay dos estilos de fallo:
- Bucle fail-fast: la app sale inmediatamente, Docker reinicia. Ruidoso, pero obvio.
- Arranques que cuelgan para siempre: el entrypoint espera la dependencia indefinidamente. Docker piensa que está “Up” pero no está sirviendo. Silencioso, pero mortal.
Prefiere esperas acotadas con timeouts explícitos. Si la dependencia no aparece, sal con código no cero y deja que on-failure intente unas pocas veces y luego pare.
Docker Compose, Swarm y por qué tu política puede no ser aplicada
El comportamiento de la política de reinicio depende de cómo despliegas.
Compose: restart: es fácil de establecer y fácil de olvidar que lo estableciste
Compose facilita esparcir restart: always por todo un archivo. Los equipos lo hacen porque “reduce tickets”. También reduce aprendizaje, hasta el día en que convierte una simple mala configuración en una tormenta de logs a nivel de flota.
Además: las diferencias entre versiones de Compose importan. Algunos campos bajo deploy: se ignoran a menos que estés usando Swarm. La gente copia/pega configs y asume que están activas. No lo están.
Servicios Swarm: el comportamiento de reinicio es un modelo diferente
Swarm tiene su propio bucle de reconciliación. Las políticas de reinicio allí forman parte del scheduling del servicio, no sólo del comportamiento local del daemon. Si estás en Swarm, usa condiciones de reinicio y delays a nivel de servicio. Si no estás en Swarm, no finjas que sí usando claves deploy: en Compose esperando que funcionen.
systemd envolviendo Docker: bucles dobles de reinicio son reales
Un patrón común: una unidad systemd ejecuta docker run y systemd tiene Restart=always. El contenedor Docker tiene --restart=always. Cuando algo falla, ambas capas “ayudan”. Ahora tienes un bucle de reinicio que persiste a reinicios del daemon y sobrevive tus intentos de detener el contenedor porque systemd lo recrea de inmediato.
Si debes usar systemd, deja que systemd controle el comportamiento de reinicio y pon la política del contenedor Docker en no. O al revés. Elige un adulto en la sala.
Observabilidad y salvaguardas: límites de tasa para tu propio caos
Una política de reinicio sin alertas es sólo un fallo silencioso con pasos extra. Tu objetivo no es “reiniciar para siempre”. Tu objetivo es “recuperar rápido de fallos transitorios y sacar a la luz fallos persistentes”. Eso significa salvaguardas.
Salvaguarda 1: alerta por reinicios y por tasa de reinicio
El conteo de reinicios por sí solo no es suficiente. Un contenedor que se reinicia una vez al día puede estar bien. Un contenedor que se reinicia 50 veces en 5 minutos es un incidente. Rastrea tanto el conteo absoluto como la tasa. Si no tienes pipeline de métricas, aún puedes hacer esto con cron + docker inspect y un archivo de estado simple. No es glamuroso, pero tampoco lo es explicar a liderazgo por qué tu factura de logging se duplicó de la noche a la mañana.
Salvaguarda 2: rotación de logs a nivel de daemon
Si estás usando json-file (muchos lo hacen), configura rotación. Bucles de reinicio + logs no acotados + discos pequeños es un generador predecible de outages.
Salvaguarda 3: reintentos acotados a nivel de política
on-failure:5 no es perfecto, pero crea un estado claro: “esto está persistentemente roto.” Ese estado es accionable. “Se reinicia para siempre” no lo es.
Salvaguarda 4: límites de recursos que reflejen la realidad
Memoria sin límites hace que un contenedor pueda tumbar el host. Memoria excesivamente ajustada lo hace reiniciarse para siempre. Ambos son malos. Establece límites razonables y monitoriza el uso real. Trata los límites como herramientas SLO, no como castigo.
Tres mini-historias del mundo corporativo (anonimizadas, plausibles, técnicamente exactas)
Mini-historia 1: El incidente causado por una suposición equivocada
En una empresa mediana, un equipo migró una API legacy de VMs a Docker en un par de hosts potentes. Estaban orgullosos: menos piezas, despliegues más sencillos, entornos consistentes. Añadieron --restart=always “para que el servicio se mantenga arriba”. Sin orquestador, sin supervisor externo. Sólo Docker Engine y confianza.
Durante una rotación rutinaria de secretos, la contraseña de la base de datos cambió. El nuevo secreto llegó al almacén de secretos, pero el job de despliegue que reconstruía contenedores falló a medias, dejando un host con la variable de entorno antigua y la imagen nueva. La API arrancó, falló la autenticación y salió con código 1. Docker la reinició. Otra vez. Y otra vez.
Los logs estaban llenos de fallos de autenticación, escritos en json-file en un disco compartido. En una hora, el disco que contenía /var/lib/docker estaba casi lleno. Luego otros contenedores no relacionados empezaron a fallar al escribir estado. La monitorización empezó a fallar porque su propio contenedor no podía escribir en disco. El on-call vio “todo se está reiniciando” y al principio sospechó un problema de kernel.
La suposición equivocada fue sutil: asumieron que una política de reinicio era una característica de fiabilidad. No lo es. Es una característica de persistencia. Un fallo persistente sigue siendo un fallo; sólo se vuelve más ruidoso con el tiempo.
La solución fue aburrida: cambiar servicios a on-failure:5, rotar logs y—lo más importante—tratar los secretos faltantes/invalidos como un fallo de despliegue que debe paginarse y tener un camino claro de rollback.
Mini-historia 2: La optimización que salió mal
Otra organización ejecutaba una flota de hosts Docker para herramientas internas. Alguien notó que los reinicios de servicio durante despliegues eran lentos porque las imágenes eran grandes y los scripts de arranque hacían “chequeos útiles”. Lo optimizaron: redujeron la imagen, quitaron un montón de chequeos y cambiaron el entrypoint por un pequeño wrapper de shell que ponía variables de entorno y lanzaba el servicio. Los despliegues se hicieron más rápidos. Todos aplaudieron.
Semanas después, una dependencia upstream empezó a devolver errores TLS intermitentes por un problema de cadena de certificados. La aplicación antes reintentaba durante un minuto antes de salir; uno de los “chequeos útiles” eliminados incluía un bucle de readiness de red. Ahora fallaba rápido y salía inmediatamente. Como el servicio tenía restart: always, la flota machacó la dependencia fallida, creando un bucle de retroalimentación. El upstream los limitó por tasa, lo que hizo que los fallos fuesen más frecuentes, lo que incrementó los reinicios, que a su vez aumentó los triggers de rate limit. Una bonita economía circular de dolor.
Empeoró: el wrapper de shell era PID 1 y no reenviaba señales correctamente. Durante la mitigación, los operadores intentaron detener los contenedores, pero los apagados fueron inconsistentes y a veces colgaban, dejando puertos ocupados. Eso hizo que reinicios posteriores fallaran de forma distinta (“address already in use”), lo que añadió confusión y alargó el incidente.
La optimización no era intrínsecamente mala—imágenes más pequeñas son buenas—pero los cambios quitaron lógica de resiliencia de la aplicación y la reemplazaron por “Docker lo reiniciará”. Docker hizo exactamente eso, y el comportamiento resultante fue técnicamente correcto y operativamente desastroso.
La solución final: restaurar la lógica de reintentos con jitter, usar entrypoints en forma exec (y un init donde fuese necesario), y cambiar la política de reinicio a reintentos acotados. También implementaron budgets de fallo upstream en el cliente para evitar estampidas contra dependencias parciales.
Mini-historia 3: La práctica aburrida pero correcta que salvó el día
Un equipo fintech ejecutaba un servicio adyacente a pagos en Docker Compose en una pequeña colección de hosts. Nada sofisticado. Lo que sí tenían era disciplina: cada servicio usaba on-failure:3 salvo excepción escrita, y cada contenedor tenía un healthcheck. Los recuentos de reinicios se enviaban como métricas y paginaban según un umbral de tasa.
Una mañana, una nueva build llegó a producción con un bug sutil de parseo de configuración. El servicio salió con código 78 (error de configuración) justo después de registrar una sola línea clara. Los primeros tres reinicios ocurrieron rápido, luego el contenedor se detuvo. El on-call recibió un pager: “servicio detenido tras reintentos”. Los logs fueron cortos y legibles porque la rotación estaba configurada globalmente. El host se mantuvo sano porque el bucle terminó por política, no por suerte.
Revirtieron en minutos. Sin disco lleno en cascada, sin “por qué la CPU está al 100%”, sin efectos de vecino ruidoso en servicios no relacionados. El postmortem fue casi aburrido, que es el mayor elogio que puedes dar a operaciones.
La práctica que los salvó no fue una herramienta exótica. Fueron dos defaults: reintentos acotados y alertas cuando se alcanza el límite. El contenedor no se “autorreparó”. El sistema se autoinformó.
Errores comunes: síntomas → causa raíz → solución
1) Síntoma: El contenedor se reinicia para siempre con la misma línea de log
Causa raíz: restart: always (o unless-stopped) + fallo determinista de arranque (env var faltante, archivo faltante, flag erróneo).
Solución: Cambia a on-failure:5, corrige la inyección de configuración y haz que tu app imprima una línea de error alta señal antes de salir.
2) Síntoma: Los reinicios muestran código 137
Causa raíz: OOM kill o terminación forzada. A menudo límite de memoria demasiado ajustado o memory leak.
Solución: Confirma logs del kernel OOM, aumenta el límite de memoria o ajusta el heap del runtime, y añade monitorización de memoria. Reintentos acotados previenen thrash en el host.
3) Síntoma: docker stop funciona, pero el contenedor vuelve
Causa raíz: La política es always (o otro supervisor lo recrea: systemd, cron, agente CI).
Solución: docker update --restart=no y busca supervisores externos. Haz que una sola capa sea responsable de los reinicios.
4) Síntoma: El contenedor está “Up” pero el servicio está muerto
Causa raíz: Sin healthcheck y el proceso está vivo pero bloqueado (deadlock, stall por dependencia). La política de reinicio no ayuda porque nada sale.
Solución: Añade healthcheck y alertas externas; considera un watchdog que reinicie ante estado persistentemente unhealthy (con cuidado), o arregla la causa del deadlock.
5) Síntoma: Tras reiniciar el host, los contenedores que “paraste” vuelven a estar corriendo
Causa raíz: restart: always ignora paradas manuales tras reinicio del daemon; unless-stopped las respeta.
Solución: Usa unless-stopped cuando la parada manual debe persistir tras reinicios del daemon, o mueve el control a un orquestador de nivel superior.
6) Síntoma: El disco se llena durante un incidente
Causa raíz: Los bucles de fallo amplifican el logging; json-file por defecto sin rotación es ilimitado.
Solución: Configura rotación de logs del daemon, reduce el spam de logging en arranque y limita los reinicios para que un fallo no genere logs infinitos.
7) Síntoma: “depends_on” no evitó el bucle de fallo
Causa raíz: El orden de arranque no es readiness. El contenedor de dependencia puede estar “up” pero no listo para aceptar conexiones.
Solución: Añade checks de readiness y lógica de reintentos; usa healthchecks y gates de readiness donde estén soportados.
8) Síntoma: No ocurre un apagado ordenado; riesgo de corrupción de datos
Causa raíz: PID 1 es un wrapper de shell que no reenvía señales; la app no maneja SIGTERM; timeout de stop demasiado corto.
Solución: Usa entrypoint en forma exec, añade un init (p. ej., --init), maneja señales y establece timeouts de stop razonables.
Listas de verificación / plan paso a paso
Paso a paso: cómo arreglar políticas de reinicio sin romper producción
- Inventario de políticas actuales. Lista contenedores y sus políticas de reinicio. Marca cualquier
alwayssin justificación clara. - Clasifica servicios. Jobs por lotes, servicios sin estado, servicios con estado, agentes de infra. Cada uno recibe un predeterminado.
- Elige una política por defecto: normalmente
on-failure:5para servicios,nopara jobs,unless-stoppedpara un pequeño conjunto de agentes que deben volver tras reboot. - Añade alertas sobre la tasa de reinicios. Si no puedes, al menos crea un informe diario y un umbral de pager para “detenido tras N reintentos”.
- Añade rotación de logs. A nivel de daemon. No confíes en que cada equipo de aplicación lo haga bien.
- Revisa entrypoints. Los wrappers de shell merecen escrutinio extra. Añade
--initdonde ayude. - Prueba modos de fallo. Corta la DB (figurativamente). Elimina un secreto. Asegúrate de que el sistema falla ruidosamente y de forma predecible.
- Despliega cambios gradualmente. Un host o un grupo de servicios a la vez. Observa dependencias ocultas en reinicios infinitos (sí, sucede).
- Documenta excepciones. Si un servicio realmente necesita
always, escribe por qué y qué alerta detecta su bucle de reinicio.
Checklist: qué capturar durante un incidente de bucle de fallo
- Política de reinicio y recuento de reinicios (
docker inspectoutput). - Último código de salida y flag OOMKilled.
- Últimas 100 líneas de log (antes de rotar o podar).
- Logs del kernel del host por OOM/disco/red.
- Uso de disco para
/var/lib/dockery puntos de montaje de volúmenes. - Cualquier configuración de supervisor externo (unidades systemd, cron jobs, runners CI).
Preguntas frecuentes
1) ¿Debo usar restart: always en producción?
Rara vez. Úsalo sólo cuando entiendas los modos de fallo y tengas alertas sobre la tasa de reinicios. Por defecto, usa on-failure:5 para servicios.
2) ¿Cuál es la diferencia práctica entre always y unless-stopped?
unless-stopped respeta una parada manual tras reinicios del daemon. always trae el contenedor de vuelta después de un reinicio del daemon incluso si lo paraste antes.
3) ¿Docker reinicia un contenedor cuando se vuelve unhealthy?
No por defecto en el Docker Engine clásico. Los healthchecks marcan estado; no disparan reinicios automáticamente. Necesitas un controlador externo si quieres ese comportamiento.
4) Si mi app sale 0 en error, ¿on-failure la reiniciará?
No. on-failure reinicia sólo en códigos de salida distintos de cero. Arregla los códigos de salida de tu app; forman parte del contrato operativo.
5) ¿Por qué no puedo hacer docker exec en un contenedor que flapea?
Porque no está corriendo lo suficiente. Desactiva reinicios (docker update --restart=no), páralo y luego ejecuta la imagen interactivamente con un shell para reproducir el fallo.
6) ¿Qué códigos de salida debería vigilar?
Señales comunes: 1 fallo genérico (revisa logs), 137 a menudo kill/OOM, 143 SIGTERM, 0 salida exitosa (pero si se reinicia, probablemente usaste always o reiniciaste el daemon).
7) ¿Pueden las políticas de reinicio ocultar outages?
Sí. Pueden convertir “servicio caído” en “servicio flapeando”, que parece vivo para monitorización superficial. Alerta por reinicios y por salud a nivel de servicio, no sólo por existencia del contenedor.
8) ¿Debo fijar un recuento máximo de reintentos?
Sí, para la mayoría de servicios. Crea un estado final estable para fallos persistentes y evita consumo infinito de recursos. Combínalo con alertas para que “detenido tras reintentos” sea accionable.
9) ¿Cuál es la mejor forma de evitar que los bucles de reinicio llenen los discos?
Acota los reinicios, rota logs a nivel de daemon y reduce logging ruidoso en arranque. También monitoriza explícitamente el uso de /var/lib/docker.
10) ¿No es Kubernetes mejor en esto?
Kubernetes te da controladores y primitivas más fuertes, pero también puede crear crash loops si configuras mal probes y backoff. El principio sigue siendo: los reinicios no arreglan fallos deterministas.
Conclusión: siguientes pasos que puedes entregar esta semana
Las políticas de reinicio son un bisturí, no cinta adhesiva. Úsalas para recuperarte de fallos transitorios, no para mantener un binario roto girando mientras tus logs llenan el disco.
Pasos prácticos:
- Audita todos los contenedores en busca de
restart: alwaysy justifica cada uno. - Cambia el predeterminado a
on-failure:5para servicios y anopara jobs. - Habilita rotación de logs a nivel de daemon si usas
json-file. - Añade alertas sobre la tasa de reinicios y sobre “detenido tras reintentos”.
- Arregla PID 1 y el manejo de señales en imágenes que usan entrypoints de shell; usa forma exec y un init cuando sea apropiado.
Entonces la próxima vez que algo falle a las 2 a.m., fallará como un adulto: una vez, claramente y con suficiente evidencia para arreglarlo.