El pager no dice “causa raíz desconocida”. Dice “servicio caído”. Y en algún lugar, un contenedor hace esa cosa embarazosa donde arranca, muere, arranca otra vez, muere otra vez — como si intentara negociar con la realidad.
Los bucles de reinicio consumen tiempo porque la gente persigue síntomas: “Docker es inestable”, “la imagen está rota”, “quizás el nodo está maldito”. Casi siempre es algo aburrido: código de salida, healthcheck, kill por OOM, temporización de dependencias, o una política de reinicio que hace exactamente lo que le pediste. Aquí tienes cómo encontrar la verdadera razón rápido, sin convertir el incidente en un estilo de vida.
Guía de diagnóstico rápido (5 minutos)
El objetivo no es “recopilar datos”. El objetivo es: identificar el disparador de reinicio y el componente que falla antes de que el bucle sobreescriba la evidencia.
Estás tratando de responder tres preguntas:
- ¿Quién lo reinicia? Política de reinicio de Docker, orquestador (Compose, Swarm), systemd, ¿o tú?
- ¿Por qué sale? Crash de la app, mala configuración, señal, kill por OOM, healthcheck fallido, dependencia ausente.
- ¿Qué cambió? Tag de la imagen, variable de entorno, secreto, volumen, presión del kernel/memoria, DNS, firewall.
Minuto 1: Identifica el contenedor y el controlador de reinicio
- Obtén
RestartCount,ExitCode,OOMKilled, y la política de reinicio. - Confirma si Compose/Swarm/systemd está involucrado.
Minuto 2: Recupera la última evidencia de fallo (antes de que se pierda)
- Revisa los logs del intento anterior (
--since/ tail). - Inspecciona
State.Errory las marcas de tiempo.
Minuto 3: Clasifica el modo de fallo
- Código de salida 1/2/126/127: problemas de app/config/ejecución.
- Código de salida 137 o
OOMKilled=true: presión de memoria. - Healthcheck “unhealthy”: la app puede seguir ejecutándose, pero el orquestador la elimina.
- Salidas instantáneas: script de entrypoint, archivo faltante, usuario/permisos incorrectos.
Minuto 4: Valida dependencias y entorno de ejecución
- DNS, red, puertos, archivos montados, permisos, secretos.
- Disponibilidad de backends (BD, cola, auth) y timeouts.
Minuto 5: Toma una decisión, no un informe
Decide cuál de estas acciones vas a ejecutar: corregir la configuración, añadir recursos, hacer rollback de la imagen, deshabilitar un healthcheck roto, o fijar dependencias.
Si no puedes decidir en cinco minutos, te falta uno de: el código de salida, evidencia de OOM, estado del healthcheck, o saber quién está reiniciando.
Una frase que debería estar en cada cerebro on-call: “La esperanza no es una estrategia.” — Gene Kranz.
Qué significa realmente “se sigue reiniciando”
Un bucle de reinicio de contenedor no es un único bug. Es un contrato entre tu proceso, Docker y quienquiera que supervise Docker.
El proceso del contenedor sale. Algo lo nota. Algo lo reinicia. Ese “algo” puede ser el propio Docker (política de reinicio), Docker Compose, Swarm, Kubernetes (si Docker es sólo el runtime), o incluso systemd que gestiona un docker run.
Así que el primer antipatrón: quedarse mirando el nombre del contenedor como si te debiera respuestas. Los contenedores no se reinician; los supervisores reinician contenedores.
El mejor movimiento de depuración es identificar al supervisor y luego leer la evidencia que deja.
Controladores de reinicio típicos
- Política de reinicio de Docker:
no,on-failure,always,unless-stopped. - Compose:
restart:endocker-compose.yml, además de problemas de orden de dependencias. - systemd: unidad con
Restart=alwaysque ejecuta Docker. - Automatización externa: cron, scripts watchdog, jobs de CI/CD que “aseguran que esté corriendo”.
Dos tipos de bucles que se sienten idénticos (pero no lo son)
Crash loop: el proceso muere rápido por problemas de app/config/recursos.
Kill loop: el proceso está en ejecución, pero un healthcheck falla o un supervisor lo mata (OOM, watchdog, política del orquestador).
Tu trabajo es separar esos dos. Los logs y los códigos de salida lo hacen en minutos—si los capturas correctamente.
Datos interesantes y breve historia (para mejorar tu intuición)
- Las políticas de reinicio de Docker llegaron temprano porque los usuarios trataban los contenedores como demonios ligeros y necesitaban algo parecido al comportamiento de init sin un init dentro del contenedor.
- El código de salida 137 suele significar SIGKILL (128 + 9). En contenedores, SIGKILL suele venir del OOM killer del kernel o de un kill forzado por un supervisor.
- Los healthchecks se añadieron después de que la gente enviara “servicios en ejecución pero muertos”—el proceso sigue vivo, pero no acepta tráfico. Sin healthchecks, esas fallas se pudren en silencio.
- Los logs de Docker no son “los logs de la app”; son lo que el proceso escribe a stdout/stderr, capturado por un driver de logs. Si tu app escribe a archivos,
docker logspuede parecer vacío aunque la app esté gritando en/var/logdentro del contenedor. - Los sistemas de archivos overlay hicieron prácticos los contenedores al permitir capas copy-on-write, pero pueden amplificar la sobrecarga de IO en cargas con muchas escrituras—provocando timeouts que parecen “reinicios aleatorios”.
- Los bucles de reinicio suelen enmascarar fallos de dependencias: la app sale porque no puede alcanzar una BD, pero la causa raíz real es DNS, firewall, mismatch TLS o contraseña rotada.
- Compose “depends_on” no significa “listo” en el Compose clásico; principalmente ordena el arranque, no la disponibilidad. Ese malentendido ha quemado a más equipos que bugs raros del kernel.
- Los kills por OOM pueden ocurrir con “memoria libre” en el host porque lo que importa para el contenedor es la contabilidad de cgroups y memoria+swap, no la RAM libre global del host.
- El comportamiento de PID 1 importa: señales, zombies y manejo de salidas pueden diferir si ejecutas una shell como PID 1 versus un wrapper tipo init, cambiando la apariencia de los reinicios.
12+ tareas prácticas: comandos, qué significa la salida y la decisión que tomas
Estos son los movimientos que puedes ejecutar bajo presión. Cada tarea incluye: un comando, qué te dice la salida y la decisión que habilita.
Ejecútalas en orden hasta dar con la prueba definitiva. Y sí, puedes hacer la mayoría de esto sin “exec” en un contenedor que muere cada cuatro segundos.
Task 1: Confirmar bucle de reinicio y obtener el ID del contenedor
cr0x@server:~$ docker ps --format 'table {{.Names}}\t{{.Image}}\t{{.Status}}\t{{.RunningFor}}'
NAMES IMAGE STATUS RUNNING FOR
api myco/api:1.24.7 Restarting (1) 8 seconds ago 2 minutes
redis redis:7 Up 3 hours 3 hours
Significado: Restarting (1) indica que Docker ve el contenedor saliendo repetidamente y aplica una política de reinicio.
El número entre paréntesis es el último código de salida (no siempre; es lo que Docker observó por última vez).
Decisión: Identifica el nombre del servicio con fallo (api) y pasa inmediatamente a inspeccionar el estado y los detalles de salida. No empieces aún con teorías de red.
Task 2: Inspeccionar política de reinicio, código de salida, OOM y marcas de tiempo
cr0x@server:~$ docker inspect api --format '{{json .State}}'
{"Status":"restarting","Running":true,"Paused":false,"Restarting":true,"OOMKilled":false,"Dead":false,"Pid":24711,"ExitCode":1,"Error":"","StartedAt":"2026-01-02T09:14:44.129885633Z","FinishedAt":"2026-01-02T09:14:51.402183576Z","Health":null}
cr0x@server:~$ docker inspect api --format 'RestartPolicy={{.HostConfig.RestartPolicy.Name}} MaxRetry={{.HostConfig.RestartPolicy.MaximumRetryCount}}'
RestartPolicy=always MaxRetry=0
Significado: ExitCode=1 es una falla a nivel de aplicación. OOMKilled=false reduce la probabilidad de kill por memoria (no es perfecto, pero es una señal fuerte).
La política de reinicio always significa que Docker seguirá intentando para siempre. Es genial hasta que no lo es.
Decisión: Enfócate en errores de arranque/configuración de la aplicación y extrae los logs del último intento. Si fuera OOMKilled=true o exit 137, pivotarías a memoria.
Task 3: Obtener los últimos logs del intento previo (no desde el inicio de los tiempos)
cr0x@server:~$ docker logs --timestamps --tail 200 api
2026-01-02T09:14:49.903214817Z level=error msg="config parse failed" err="missing ENV DATABASE_URL"
2026-01-02T09:14:49.903955113Z level=error msg="fatal: cannot start without database"
Significado: El contenedor no se está “reiniciando misteriosamente”. Está fallando de forma determinista: falta la variable de entorno DATABASE_URL.
Decisión: Arregla la configuración y vuelve a desplegar. Para. No añadas memoria. No “reconstruyas la imagen”. No culpes a Docker.
Task 4: Confirmar las env del contenedor y lo que cree tener
cr0x@server:~$ docker inspect api --format '{{range .Config.Env}}{{println .}}{{end}}' | sed -n '1,12p'
APP_ENV=prod
LOG_LEVEL=info
PORT=8080
Significado: El entorno carece de lo que los logs dicen que falta. Eso es consistente. Bien.
Decisión: Determina de dónde debería venir la env: archivo Compose, --env-file, inyección de secretos, o herramienta de plataforma. Arregla en la fuente, no con un docker exec puntual.
Task 5: Si es Compose, verifica la configuración renderizada
cr0x@server:~$ docker compose config | sed -n '/services:/,$p' | sed -n '1,120p'
services:
api:
environment:
APP_ENV: prod
LOG_LEVEL: info
PORT: "8080"
image: myco/api:1.24.7
restart: always
Significado: La configuración de Compose carece de DATABASE_URL. Quizás no se cargó el env file, o el nombre de la variable cambió.
Decisión: Corrige docker-compose.yml o la ruta del env file. Luego redepliega con un recreate limpio para que la configuración antigua no persista.
Task 6: Buscar reinicios impulsados por healthcheck (es más escurridizo de lo que crees)
cr0x@server:~$ docker inspect api --format '{{json .State.Health}}'
{"Status":"unhealthy","FailingStreak":5,"Log":[{"Start":"2026-01-02T09:20:10.001712312Z","End":"2026-01-02T09:20:10.045221991Z","ExitCode":7,"Output":"curl: (7) Failed to connect to localhost port 8080: Connection refused\n"}]}
Significado: El proceso podría estar corriendo, pero el healthcheck no alcanza el servicio. El código de salida 7 de curl es “falló conectar”.
Algunos entornos (especialmente wrappers de Compose o supervisores externos) reiniciarán contenedores unhealthy.
Decisión: Decide si el healthcheck está mal (revisa puerto/interfaz), es demasiado agresivo (intervalo/timeout), o detecta correctamente una app muerta. Entonces corrige el healthcheck o el binding del servicio.
Task 7: Determinar si intervino el OOM killer del kernel
cr0x@server:~$ docker inspect api --format 'ExitCode={{.State.ExitCode}} OOMKilled={{.State.OOMKilled}} Error={{.State.Error}}'
ExitCode=137 OOMKilled=true Error=
cr0x@server:~$ dmesg -T | tail -n 20
[Thu Jan 2 09:25:13 2026] oom-kill:constraint=CONSTRAINT_MEMCG,nodemask=(null),cpuset=docker.service,mems_allowed=0,oom_memcg=/docker/3b2e...,task_memcg=/docker/3b2e...,task=myco-api,pid=31244,uid=1000
[Thu Jan 2 09:25:13 2026] Killed process 31244 (myco-api) total-vm:2147488kB, anon-rss:612344kB, file-rss:1420kB, shmem-rss:0kB
Significado: Ahora es una clase distinta de problema. El contenedor no “se cayó”; le dispararon.
OOMKilled=true junto con dmesg confirma que el kernel mató el proceso bajo presión de memoria en el cgroup.
Decisión: Aumenta el límite de memoria, reduce el uso de memoria, o corrige una fuga/regresión. También revisa la contención de memoria a nivel de nodo y vecinos ruidosos.
Task 8: Revisar límites de memoria del contenedor y uso actual
cr0x@server:~$ docker inspect api --format 'Memory={{.HostConfig.Memory}} MemorySwap={{.HostConfig.MemorySwap}}'
Memory=536870912 MemorySwap=536870912
cr0x@server:~$ docker stats --no-stream --format 'table {{.Name}}\t{{.MemUsage}}\t{{.MemPerc}}\t{{.CPUPerc}}'
NAME MEM USAGE / LIMIT MEM % CPU %
api 510MiB / 512MiB 99.6% 140.3%
redis 58MiB / 7.7GiB 0.7% 0.4%
Significado: Un límite de 512MiB con swap igual a la memoria es ajustado; básicamente no permites margen. CPU al 140% sugiere trabajo intenso (múltiples hilos).
Decisión: Si ese límite fue intencional, afina la app (tamaños de heap, caches) y verifica perfil de memoria. Si fue accidental, elévalo y continúa.
Task 9: Identificar problemas de salida rápida: entrypoint erróneo, binario faltante, permisos
cr0x@server:~$ docker inspect api --format 'Entrypoint={{json .Config.Entrypoint}} Cmd={{json .Config.Cmd}} User={{json .Config.User}}'
Entrypoint=["/docker-entrypoint.sh"] Cmd=["/app/server"] User="10001"
cr0x@server:~$ docker logs --tail 50 api
/docker-entrypoint.sh: line 8: /app/server: Permission denied
Significado: El binario existe pero no es ejecutable para el usuario configurado, o el sistema de archivos está montado con noexec, o la build de la imagen perdió los bits ejecutables.
Decisión: Corrige permisos en la imagen (chmod +x en build), o ejecuta como un usuario que pueda ejecutar, o elimina noexec de las opciones de montaje para ese volumen. No “arregles” esto con chmod dentro de un contenedor en ejecución; no sobrevivirá a las rebuilds.
Task 10: Validar mounts y si un volumen oculta tus archivos embebidos
cr0x@server:~$ docker inspect api --format '{{range .Mounts}}{{println .Destination "->" .Source "type=" .Type}}{{end}}'
/app -> /var/lib/docker/volumes/api_app/_data type= volume
/config -> /etc/myco/api type= bind
Significado: Montar un volumen en /app puede enmascarar el binario de la aplicación incluido en la imagen. Si el volumen está vacío o anticuado, el contenedor arranca en un directorio vacío y muere.
Decisión: No montes sobre la ruta de tu aplicación salvo que lo quieras. Mueve datos escribibles a /data o similar. Si necesitas hot-reload en dev, mantenlo sólo en dev.
Task 11: Ver el stream de eventos para ver quién lo mata/reinicia
cr0x@server:~$ docker events --since 10m --filter container=api | tail -n 20
2026-01-02T09:31:10.002345678Z container die 3b2e... (exitCode=137, image=myco/api:1.24.7, name=api)
2026-01-02T09:31:10.120456789Z container start 3b2e... (image=myco/api:1.24.7, name=api)
Significado: Los eventos muestran ciclos die/start explícitos y códigos de salida. Si ves eventos kill con atribución de usuario/daemon (a veces en logs de auditoría), ese es tu supervisor externo o una acción de operador.
Decisión: Si los reinicios son impulsados por política, arregla la causa de salida subyacente. Si son manuales/automatizados, encuentra la automatización y evita que te combata.
Task 12: Revisar systemd si Docker está siendo supervisado desde fuera
cr0x@server:~$ systemctl status myco-api.service --no-pager
● myco-api.service - MyCo API container
Loaded: loaded (/etc/systemd/system/myco-api.service; enabled; vendor preset: enabled)
Active: activating (auto-restart) (Result: exit-code) since Thu 2026-01-02 09:33:12 UTC; 4s ago
Process: 32511 ExecStart=/usr/bin/docker run --rm --name api myco/api:1.24.7 (code=exited, status=1/FAILURE)
Main PID: 32511 (code=exited, status=1/FAILURE)
Significado: systemd está reiniciando el runner del contenedor, no Docker en sí. Tu bucle de reinicio podría ni siquiera ser una política de Docker.
Decisión: Corrige el unit file (entorno, mounts, backoff de reinicio). También decide si debería gestionarse con Compose en su lugar, para evitar dos supervisores en tiras y aflojas.
Task 13: Reproducir sin reinicio para preservar evidencia
cr0x@server:~$ docker inspect api --format 'RestartPolicy={{.HostConfig.RestartPolicy.Name}}'
RestartPolicy=always
cr0x@server:~$ docker update --restart=no api
api
cr0x@server:~$ docker start -a api
2026-01-02T09:35:01.110Z level=error msg="fatal: cannot open /config/app.yaml" err="permission denied"
Significado: Deshabilitar el reinicio detiene el bucle y te permite adjuntarte al fallo. Esa suele ser la forma más rápida de dejar de perder logs.
Decisión: Usa esto mientras depuras. Luego restaura la política de reinicio deseada después de la corrección. No dejes servicios de producción con restart deshabilitado salvo que disfrutes sorpresas nocturnas.
Task 14: Validar permisos y mapeo de usuario en bind mounts
cr0x@server:~$ ls -l /etc/myco/api/app.yaml
-rw------- 1 root root 2180 Jan 2 09:00 /etc/myco/api/app.yaml
cr0x@server:~$ docker inspect api --format 'User={{.Config.User}}'
10001
Significado: El contenedor corre como UID 10001, pero el archivo montado está legible sólo por root. Es un modo de fallo limpio y aburrido.
Decisión: Corrige la propiedad/permisos en el archivo del host, o ejecuta el contenedor con un usuario que pueda leerlo, o usa mecanismos de secretos/config diseñados para esto.
Task 15: Comprobar DNS/red desde un contenedor debug en la misma red
cr0x@server:~$ docker network ls
NETWORK ID NAME DRIVER SCOPE
6c0f1b1e2c0a myco_default bridge local
cr0x@server:~$ docker run --rm --network myco_default busybox:1.36 nslookup postgres
Server: 127.0.0.11
Address 1: 127.0.0.11
Name: postgres
Address 1: 172.19.0.3 postgres.myco_default
Significado: DNS dentro de la red Docker resuelve postgres. Si tu app dice “host not found”, el problema puede ser la configuración de la app o que está en otra red.
Decisión: Si DNS falla aquí, arregla la red Docker o el nombre del servicio. Si DNS funciona, pivota a credenciales, TLS, firewall o temporización de readiness.
Chiste corto #1: Un contenedor en bucle de reinicio es la forma de DevOps de enseñar paciencia, repetidamente y con convicción.
Códigos de salida que deberías memorizar
Los códigos de salida son lo más cercano a una confesión. Docker muestra el código de salida, pero aún debes interpretarlo con las convenciones de Unix y las realidades específicas de contenedores.
Los útiles
- 0: salida limpia. Si se reinicia igual, alguien se lo indicó.
- 1: error genérico. Mira los logs; suele ser configuración o excepción lanzada.
- 2: uso incorrecto de builtins/CLI; a menudo flags incorrectos o errores en scripts de entrypoint.
- 125: Docker no pudo ejecutar el contenedor (error del daemon, opciones inválidas). Esto no es tu app.
- 126: comando invocado no puede ejecutarse (permisos, arquitectura equivocada, montaje noexec).
- 127: comando no encontrado (ENTRYPOINT/CMD mal, shell faltante, PATH incorrecto).
- 128 + N: el proceso murió por la señal N. Comunes: 137 (SIGKILL=9), 143 (SIGTERM=15).
- 137: SIGKILL. A menudo OOM killer, a veces kill forzado por un watchdog.
- 139: SIGSEGV. Crash nativo; puede ser libc mala, binario corrupto o corrupción de memoria.
Cómo los códigos de salida se combinan con las políticas de reinicio
La política on-failure se activa con códigos de salida distintos de cero. Eso significa que no reiniciará en salida 0.
La política always no se fija; reinicia sin importar qué, lo que puede ocultar una app que termina intencionalmente tras completar su trabajo.
Si ejecutas un contenedor tipo job (migraciones, cron, batch), always suele ser incorrecto. Si ejecutas un servicio, always está bien—hasta que despliegas algo que sale inmediatamente y pierdes el contexto de logs en el ciclo.
Healthchecks: cuando “healthy” se convierte en detector de mentiras
Los healthchecks son buenos. Los malos healthchecks generan caos.
También suelen ser malentendidos: el healthcheck integrado de Docker no reinicia contenedores por sí mismo. Pero muchos supervisores y patrones de despliegue tratan “unhealthy” como “matar y reiniciar”.
Cómo fallan los healthchecks en producción
- Interfaz/puerto equivocado: la app se enlaza a
0.0.0.0pero el healthcheck apunta alocalhostincorrectamente—o al revés. - Tiempo de arranque: el healthcheck comienza antes de que la app esté lista, provocando una racha de fallos y reinicios.
- Acoplamiento de dependencias: el healthcheck llama a servicios downstream. Cuando esos están caídos, tu contenedor es eliminado aunque pueda servir tráfico parcial.
- Picos de recursos: healthcheck demasiado frecuente; en un nodo estresado, lo derrumba.
- Usar curl en imágenes minimalistas: comando del healthcheck no encontrado genera exit 127, que parece “app muerta” cuando sólo falta curl.
Qué hacer
Los healthchecks deben probar la capacidad de tu servicio para servir, no todo el universo.
Si una BD está caída, es válido que la app reporte unhealthy—si la app no puede funcionar sin ella. Pero no incluyas cada dependencia externa en el endpoint de salud salvo que estés seguro de que reiniciar ayuda.
Ajusta start_period (si está disponible), intervalos y timeouts. Más importante: mantén los healthchecks deterministas y baratos.
Trampas de almacenamiento y rendimiento que parecen caídas
Como persona de almacenamiento, diré lo obvio: muchos incidentes de “contenedor sigue reiniciando” son realmente “IO se volvió lento, ocurrieron timeouts, el proceso salió”.
A Docker no le importa por qué salió tu proceso. Solo ve una salida. Tu app puede salir por una migración fallida, timeout de lock, o “disco lleno”.
Disco lleno: el clásico que nunca muere
Los contenedores escriben logs, archivos temporales, archivos de base de datos (a veces por accidente) y diffs de capas. Si el filesystem root de Docker se llena, los contenedores empiezan a fallar de maneras encantadoras:
fallos de write(), archivos temporales corruptos, bases de datos que se niegan a arrancar, o tu app colapsando porque no puede escribir un archivo PID.
cr0x@server:~$ df -h /var/lib/docker
Filesystem Size Used Avail Use% Mounted on
/dev/nvme0n1p4 80G 79G 320M 100% /var/lib/docker
Significado: Te quedaste sin pista. Espera fallos aleatorios.
Decisión: Libera espacio (prune imágenes/volúmenes con cuidado), mueve Docker root a un filesystem mayor, o deja de escribir archivos grandes en capas de contenedor. Luego corrige la retención de logs para que no vuelva a ocurrir.
Amplificación de escrituras en overlay y “solo se reinicia bajo carga”
Overlay2 está bien para la mayoría, pero si tu carga escribe mucho en la capa de filesystem del contenedor (no en volúmenes), el rendimiento puede colapsar.
Cuando la latencia sube, los timeouts se encadenan: la app falla readiness, los healthchecks fallan, el orquestador mata, ocurren reinicios.
Consejo práctico: los datos escribibles van a volúmenes. Los logs van a stdout/stderr (y son recogidos por un driver de logs sensato), no a un archivo dentro de la capa de la imagen. Las bases de datos no deberían escribir en overlay a menos que disfrutes aprender sobre latencia de fsync a las 3 a.m.
Permisos y desajustes de UID en bind mounts
La buena práctica de seguridad es “correr sin root”. Perfecto. Luego montas archivos del host propiedad de root y te preguntas por qué se reinicia.
Esto no es que Docker sea malo. Es Linux siendo Linux.
Reloj y fallos TLS: la dependencia no obvia
Si el reloj del host se desplaza, TLS puede fallar. Las apps que tratan “no se puede establecer TLS” como fatal pueden salir inmediatamente. Eso parece un bucle de reinicio con exit code 1.
Si ves reinicios generalizados repentinos entre servicios que usan TLS, revisa NTP/chrony y las ventanas de validez de los certificados.
Tres microhistorias corporativas reales
1) Incidente por una suposición errónea: “depends_on significa ready”
Una empresa mediana ejecutaba una stack de Compose: API, worker, Postgres, Redis. Había sido estable durante meses, hasta un reinicio rutinario del host durante mantenimiento.
Tras el reinicio, el contenedor de la API empezó a flapear. Se reiniciaba cada pocos segundos. Los ingenieros culparon a una “imagen mala” porque el deploy había ocurrido un día antes.
La suposición equivocada fue sutil y común: creían que depends_on significaba que Postgres estaba listo para aceptar conexiones. En realidad, Compose arrancó Postgres primero, pero Postgres aún necesitaba tiempo para recuperación de crash e inicialización.
La API intentó ejecutar migraciones al arrancar, no pudo conectar y salió con código 1. Docker la reinició diligentemente. Una y otra vez.
Los logs estaban ahí, pero enterrados—porque el bucle de reinicio era rápido y la salida de logs se mezclaba con líneas no relacionadas. El equipo inicialmente persiguió problemas de red: “¿Está roto DNS de Docker tras el reboot?” No. Ejecutaron un contenedor debug y verificaron que DNS y conectividad estaban bien.
La solución fue aburrida y correcta: añadir lógica de reintento con backoff exponencial para la conexión BD al arrancar, y separar las migraciones en un job one-shot con salida de error clara.
También añadieron un healthcheck a Postgres y hicieron que la API espere readiness (o al menos maneje “no listo” sin salir).
El incidente terminó no con heroísmos, sino con una admisión: el orden de orquestación no es readiness, y la fiabilidad viene de diseñar el arranque para tolerar el mundo real.
2) Optimización que salió mal: “Podemos reducir límites de memoria para ahorrar”
Otra organización quiso reducir costes de infraestructura y “apretar el uso de recursos”. Bajaron límites de memoria de contenedores en varios servicios.
Parecía bien en staging, donde el tráfico era bajo y caches fríos. Producción no es staging. Producción nunca lo es.
En un día, una API clave empezó a reiniciarse de forma intermitente. No constantemente—lo suficiente como para hacer el monitoreo ruidoso y molestar a clientes.
El código de salida fue 137. OOM kills. El servicio usaba un runtime gestionado con heap adaptativo, más una carga bursty JSON que provocaba asignaciones transitorias.
Los ingenieros primero intentaron afinar el GC del runtime y limitar el heap. Eso ayudó, pero se perdieron un efecto de segundo orden: una nueva “optimización” en el mismo cambio aumentó el nivel de compresión en respuestas para ahorrar ancho de banda.
La CPU subió, la latencia subió, y porque las peticiones se acumularon, la presión de memoria empeoró. Los OOM aumentaron.
La solución real fue deshacer el cambio de compresión para ese endpoint y restaurar un límite de memoria realista con margen. Luego perfilaron asignaciones bajo carga similar a producción y aplicaron reducciones dirigidas.
La lección quedó: límites “eficientes” que desencadenan OOM no son eficientes; son un impuesto pagado en incidentes.
3) Práctica aburrida pero correcta que salvó el día: detener el bucle y preservar evidencia
Una compañía financiera tenía un servicio containerizado que empezó a reiniciarse tras una rotación de certificados.
Su runbook on-call incluía una línea simple: “Si un contenedor está flapando, deshabilita reinicio y ejecútalo adjunto una vez para capturar el error fatal.”
No fue glamoroso. No parecía brujería. Funcionó.
Ejecutaron docker update --restart=no y luego docker start -a. La app imprimió de inmediato un error TLS claro: no podía leer la nueva clave privada.
La clave se había desplegado con permisos restrictivos en un bind mount, legible sólo por root, mientras el contenedor corría como UID sin root.
Sin parar el bucle, los logs habrían sido parciales y sobreescritos por reinicios repetidos. Con el bucle detenido, el mensaje de fallo fue inconfundible.
Arreglaron la propiedad del archivo, reiniciaron el servicio y siguieron con su vida.
La mejora siguiente fue aún más aburrida: cambiaron el despliegue de certificados para usar un mecanismo de secretos con permisos correctos por defecto, y añadieron una comprobación de arranque que informa un error claro antes de intentar servir tráfico.
Errores comunes: síntoma → causa raíz → solución
Esta sección es el catálogo de “ya vi esta película”. Si estás de guardia, escanea los síntomas, elige la causa probable y pruébala con una de las tareas anteriores.
1) Reinicios cada 2–10 segundos, exit code 1
- Síntoma:
Restarting (1), los logs muestran errores de configuración o env faltante. - Causa raíz: variable de entorno faltante, flag erróneo, secretos no montados, fallo de parseo de configuración.
- Solución: corregir inyección de env en Compose; validar con
docker compose config; redeploy con recreate.
2) Reinicios con exit code 127 o “command not found”
- Síntoma: logs:
exec: "foo": executable file not found in $PATH. - Causa raíz: CMD/ENTRYPOINT equivocado, binario faltante, usar imagen Alpine sin bash pero el entrypoint usa
/bin/bash. - Solución: arreglar el entrypoint en el Dockerfile; preferir la forma exec; asegurar que el shell requerido exista o eliminar la dependencia de shell.
3) Reinicios con exit code 126 o “permission denied”
- Síntoma: el binario existe pero no es ejecutable, o el script de entrypoint no es ejecutable.
- Causa raíz: modo de archivo incorrecto, montaje con
noexec, propiedad equivocada al ejecutar sin root. - Solución: establecer bit ejecutable en build; ajustar opciones de montaje; alinear UID/GID o permisos.
4) Exit code 137, esporádico bajo carga
- Síntoma:
OOMKilled=trueen inspect; dmesg muestra oom-kill. - Causa raíz: límite de memoria demasiado bajo; fuga de memoria; picos de carga; poca swap permitida.
- Solución: subir límite, afinar heap/caches, investigar perfil de memoria; reducir concurrencia; añadir backpressure.
5) “Up” pero cambia constantemente entre healthy/unhealthy y luego reinicia
- Síntoma: healthcheck fallando en rachas; reinicios si el orquestador reacciona a unhealthy.
- Causa raíz: healthcheck agresivo, endpoint equivocado, comprobando dependencias downstream, app enlaza a otra interfaz.
- Solución: hacer el healthcheck barato y correcto; ajustar intervalo/timeout/start period; asegurar que la app escuche como se espera.
6) Funciona en un host, flapea en otro
- Síntoma: misma imagen, comportamiento diferente.
- Causa raíz: diferencias de kernel/cgroup, filesystem lleno, permisos de montaje distintos, configuración DNS, mismatch de arquitectura CPU.
- Solución: comparar
docker info, espacio en disco del host, opciones de montaje; verificar arch de la imagen; estandarizar runtime.
7) El contenedor sale “exitosamente” (code 0) pero se reinicia sin parar
- Síntoma: código de salida 0; política de reinicio
always. - Causa raíz: estás ejecutando un job (migraciones, init, CLI) con política de servicio.
- Solución: usar
restart: "no"oon-failure; separar job de servicio de larga duración.
8) Después de desplegar un “cambio pequeño”, todo empieza a flappear
- Síntoma: múltiples contenedores se reinician al mismo tiempo.
- Causa raíz: dependencia compartida: outage DNS, rotación de certificados, deriva del reloj, throttling de registry, disco lleno, presión de memoria del host.
- Solución: revisar señales del host (disco, dmesg, sincronización de tiempo); verificar permisos de secretos/certificados; revertir el cambio compartido.
Chiste corto #2: Si tu healthcheck depende de cinco servicios, no es un healthcheck—es un proyecto grupal.
Listas de verificación / plan paso a paso
Checklist A: Detener la hemorragia (seguro para producción)
- Confirmar alcance: es un contenedor o varios? Si son muchos, sospecha problemas a nivel de host (disco, memoria, DNS, tiempo).
- Capturar evidencia: obtener
docker inspectState, las últimas 200 líneas de log ydocker eventsde 10 minutos. - Estabilizar: si el bucle es muy rápido, deshabilita reinicio temporalmente (
docker update --restart=no) para preservar logs y reducir churn. - Elegir acción: rollback del tag de imagen, arreglar env/secretos, subir memoria, o corregir healthcheck.
- Comunicar claramente: “Exit code 137, OOM kill confirmado en dmesg. Subiendo límite y revirtiendo cambio de memoria.” No “Docker parece raro.”
Checklist B: Encontrar el disparador de forma limpia y reproducible
- Identificar política de reinicio y supervisor (Docker vs Compose vs systemd).
- Leer el código de salida y la bandera OOM.
- Leer logs del último intento (
--tail, con timestamps). - Revisar los logs de estado de healthchecks si están configurados.
- Verificar mounts y permisos (especialmente al ejecutar sin root).
- Verificar conectividad de dependencias desde la misma red Docker.
- Comprobar espacio en disco del host y logs OOM del host.
- Reejecutar el contenedor adjunto una vez con reinicio deshabilitado para reproducir limpiamente.
Checklist C: Prevenir recurrencia (la parte que la gente salta)
- Hacer el arranque resistente: reintentos con backoff; no salir instantáneamente por fallos transitorios.
- Separar jobs one-shot: migraciones y cambios de esquema deben ser jobs explícitos, no ocultos en el arranque del servicio principal.
- Dimensionar bien los healthchecks: baratos, deterministas, no dependientes de todo el mundo.
- Establecer límites sensatos: margen de memoria, restricciones CPU adecuadas a la carga, y evitar límites basados en optimismo.
- Enviar logs a stdout/stderr: mantener logs accesibles y centralizables.
- Documentar invariantes: vars de entorno requeridas, mounts necesarios, permisos esperados, comportamiento de salida esperado.
Qué evitar cuando depuras un bucle de reinicio
- No sigas reconstruyendo imágenes hasta que puedas indicar el código de salida y la última línea fatal del log.
- No hagas exec en el contenedor primero; puede morir antes de que aprendas algo. Empieza con inspect/logs/events.
- No pongas “restart: always” por todas partes como parche. Oculta contenedores job y puede amplificar tormentas de fallos.
- No culpes a Docker hasta que hayas comprobado disco lleno y OOM. Docker mayormente sólo reporta lo que Linux hizo.
Preguntas frecuentes (FAQ)
1) ¿Por qué docker ps muestra “Restarting (1)”?
Significa que el contenedor está saliendo y Docker aplica una política de reinicio. El número es el último código de salida observado.
Confirma con docker inspect y lee .State.ExitCode junto con las marcas de tiempo.
2) ¿Cómo sé si Docker lo está reiniciando o alguien más?
Revisa la política de reinicio en docker inspect (.HostConfig.RestartPolicy), luego busca supervisores externos:
systemctl status para unit files, y docker events para patrones de start/stop. Compose también añade su propio comportamiento de ciclo de vida.
3) ¿Cuál es la forma más rápida de atrapar el mensaje de error real?
Deshabilita reinicio temporalmente (docker update --restart=no) y ejecútalo adjunto una vez (docker start -a).
Esto detiene el churn y muestra la línea fatal claramente.
4) Exit code 137: ¿siempre es OOM?
No. Significa SIGKILL. OOM es la causa más común en contenedores, pero un supervisor también puede SIGKILL a un proceso.
Confirma con docker inspect (OOMKilled=true) y logs del host (dmesg).
5) ¿Por qué docker logs está vacío aunque la app falla?
Porque Docker sólo captura stdout/stderr. Si tu app escribe a archivos dentro del contenedor, docker logs puede estar silencioso.
Reconfigura la app para loguear a stdout/stderr o inspecciona los archivos (idealmente vía un volumen, no la capa del contenedor).
6) El contenedor está “unhealthy” pero el proceso corre. ¿Por qué reiniciar?
El estado de salud de Docker no reinicia inherentemente el contenedor, pero muchos patrones de despliegue sí: supervisores externos, scripts o orquestadores interpretan unhealthy como “reemplazar”.
Arregla el endpoint de healthcheck, su temporización o la sensibilidad, o ajusta el comportamiento del supervisor.
7) ¿Por qué funciona cuando lo ejecuto manualmente pero no bajo Compose?
Compose cambia redes, inyección de entorno, mounts de volúmenes y a veces el directorio de trabajo. Compara:
docker compose config vs docker inspect para el contenedor en ejecución. Las diferencias en mounts y env vars son los culpables habituales.
8) ¿Cómo depuro un contenedor que sale demasiado rápido para hacer exec?
Usa docker logs y docker inspect primero. Si es necesario, deshabilita reinicio y ejecútalo adjunto una vez.
También puedes sobreescribir el entrypoint temporalmente para obtener un shell e inspeccionar el filesystem, pero trátalo como un experimento controlado, no como la solución definitiva.
9) ¿Los problemas de disco realmente pueden causar bucles de reinicio?
Absolutamente. Disco lleno, IO lento, o problemas de permisos en volúmenes pueden hacer que las apps fallen checks de arranque, se caigan o hagan timeouts.
Revisa df -h en Docker root, inspecciona mounts y busca “no space left on device” en logs.
10) ¿Sobre qué debo alertar para detectarlo temprano?
Alerta por incremento en contadores de reinicio, flips de estado de salud, eventos OOM kill, uso de disco en Docker root y tasas elevadas de salida de contenedores.
Los reinicios no son inherentemente malos; los reinicios inesperados sí. Mide tu normal.
Conclusión: pasos siguientes que evitan la secuela
Un bucle de reinicio de contenedor parece caótico, pero suele ser determinista. Deja de adivinar.
En cinco minutos puedes saber: quién lo reinicia, qué código de salida devuelve, si fue kill por OOM y qué dijo la última línea fatal del log.
Después de eso, la solución suele ser mundana: corregir env/secretos, arreglar permisos, ajustar comportamiento del healthcheck, o dar al proceso suficiente memoria para vivir.
Haz esto a continuación:
- Estandariza un runbook: inspect → logs → events → señales del host (disco/OOM) → reproducir adjunto una vez.
- Haz el arranque tolerante: reintentos con backoff, timeouts y mensajes fatales claros.
- Separa jobs de servicios; no ejecutes migraciones como efecto lateral del boot del servicio salvo que realmente lo quieras.
- Audita las políticas de reinicio: usa
alwayspara servicios,on-failurepara jobs que fallen, ynopara one-shots. - Mueve datos escribibles a volúmenes y mantiene logs accesibles en stdout/stderr.
No necesitas heroísmos. Necesitas evidencia, rápido. Luego necesitas la disciplina para cambiar lo que realmente se rompió.