Docker: Por qué tu contenedor se reinicia para siempre (y el único registro que necesitas)

¿Te fue útil?

Desployas un contenedor. Parece estar bien por un segundo. Luego muere. Entonces Docker, amablemente, lo vuelve a traer a la vida… para que muera otra vez. El ciclo continúa hasta que tu monitor parece el electrocardiograma de una película mala y tu teléfono de guardia empieza a negociar contigo.

Esta es la parte en la que la gente pierde horas mirando docker ps y adivinando. No lo hagas. Cuando un contenedor se reinicia para siempre, hay un registro que te dice de forma fiable qué pasó: los registros del intento anterior, no el actual que todavía está arrancando.

El único registro que necesitas (y por qué no es el que estás mirando)

Cuando un contenedor se reinicia, frecuentemente muere rápido—a veces antes de emitir algo útil, otras veces después de imprimir la línea útil y salir inmediatamente. Si haces tail de los logs durante el bucle de reinicio, tiendes a atrapar el momento equivocado: la instancia nueva arrancando (otra vez), no la que acaba de fallar.

El registro que necesitas es el intento de contenedor anterior:

cr0x@server:~$ docker logs --previous myservice
error: cannot open config file /etc/myapp/config.yaml: permission denied

Si recuerdas solo una cosa de este artículo, recuerda ese flag. Es la diferencia entre “creo que es red” y “es un error de permisos de archivo, arregla el montaje”.

Por qué funciona: el flujo de logs de Docker está ligado al ciclo de vida del contenedor. Cuando el contenedor se recrea o se reinicia (dependiendo del escenario y del runtime), quieres la stdout/stderr de la última ejecución. Eso es lo que te da --previous para los bucles donde el contenedor se está reiniciando bajo el mismo nombre.

Y sí, hay matices. Si usas Compose y los contenedores se están recreando (nuevos IDs) en lugar de reiniciarse, puede que necesites coger los logs por ID de contenedor o usar docker compose logs. Pero el principio sigue: deja de mirar el intento de arranque actual e inspecciona el último crash.

Qué significa realmente “se reinicia para siempre” en Docker

Un bucle de reinicio no es una sola cosa. Es una familia de comportamientos que desde lejos parecen igual: el contenedor aparece como “Restarting (x) …” o sigue reapareciendo en docker ps.

Políticas de reinicio: la letra pequeña que la gente se salta

Los bucles de reinicio suelen ser impulsados por una política de reinicio. Docker soporta:

  • no (por defecto): sale y se queda muerto.
  • on-failure[:max-retries]: reinicia cuando el código de salida es distinto de cero.
  • always: reinicia independientemente del código de salida (excepto cuando lo detienes).
  • unless-stopped: reinicia salvo que lo detengas explícitamente.

En producción, normalmente quieres unless-stopped para servicios de larga ejecución. Pero los “buenos valores por defecto” se vuelven “ruido malo” cuando el proceso se cae al instante. La política reinicia fielmente algo roto. Como cualquier empleado diligente, hace exactamente lo que pediste, no lo que querías.

Los códigos de salida son tu primera pista real

Docker no reinicia un contenedor porque esté aburrido. Lo reinicia porque el proceso principal termina. Ese proceso termina por una de tres razones generales:

  • La app decide salir (error de configuración, fallo de migración, dependencia ausente).
  • El SO la mató (OOM killer, SIGKILL, restricciones de cgroup).
  • Tú lo orquestaste (falla de healthcheck, watchdog, unidad systemd).

El código de salida y la bandera “OOMKilled” te indican en qué rama estás. No estás diagnosticando “Docker”. Estás diagnosticando por qué el PID 1 dentro de ese contenedor no puede mantenerse vivo.

Una cita para mantener en la cabeza mientras debuggeas: “La esperanza no es una estrategia.” — General Gordon R. Sullivan. No es estrictamente una cita SRE, pero aplica brutalmente bien a los bucles de reinicio.

Guion de diagnóstico rápido (primero/segundo/tercero)

Este es el guion que uso cuando un contenedor está oscilando y quiero encontrar el cuello de botella rápido, sin convertir el incidente en un proyecto de investigación.

Primero: captura la última falla (no mires el arranque actual)

  1. Obtén los logs anteriores: docker logs --previous (o por ID de contenedor).
  2. Obtén el código de salida y la razón: docker inspect para State.ExitCode, State.OOMKilled, State.Error.

Si los logs anteriores muestran un error claro de configuración, para. Arregla eso. No “añadas más memoria” por un error tipográfico en YAML.

Segundo: determina si es un crash, un kill o un reinicio deliberado

  1. Revisa dmesg / journal por kills OOM.
  2. Revisa el estado del healthcheck (unhealthy puede provocar reinicios del orquestador incluso si el proceso sigue activo).
  3. Comprueba quién lo está reiniciando: política de reinicio de Docker, systemd, Compose, Swarm.

Tercero: valida el entorno de ejecución (almacenamiento, red, dependencias)

  1. Montajes y permisos: bind mounts, secretos, archivos de configuración.
  2. Puertos y DNS: ¿falla al enlazar, falla al resolver, falla TLS?
  3. Límites de recursos: memoria, pids, ulimits, espacio en disco, agotamiento de inodos.

Broma #1: Los contenedores son como plantas de interior—ignora lo básico (agua, luz, tierra) y morirán puntualmente.

Tareas prácticas: comandos, salidas y decisiones (12+)

A continuación están las tareas que realmente te hacen avanzar. Cada una incluye un comando realista, una salida de ejemplo, qué significa y qué decisión tomar después. Ejecuta estas en el host a menos que se indique lo contrario.

Tarea 1: Ver el bucle de reinicio y coger el ID del contenedor

cr0x@server:~$ docker ps --no-trunc
CONTAINER ID                                                       IMAGE               COMMAND                  CREATED          STATUS                         PORTS                    NAMES
b5a1c0b7fd4b1f7b4b5d5c6a9c8d2d9c7a1c2e3f4a5b6c7d8e9f0a1b2c3d4e5   myapp:1.4.2         "/entrypoint.sh"         2 minutes ago    Restarting (1) 5 seconds ago                            myservice

Significado: Docker informa “Restarting” con un código de salida entre paréntesis. Ese número suele ser el último código de salida.

Decisión: Pasa inmediatamente a los logs anteriores e inspecciona el estado.

Tarea 2: Recupera el único registro que necesitas

cr0x@server:~$ docker logs --previous myservice
[2026-02-04T10:15:02Z] FATAL: DB_URL is not set
[2026-02-04T10:15:02Z] exiting with code 2

Significado: La app está saliendo limpiamente pero con fallo por falta de una variable de entorno.

Decisión: Arregla la configuración en la capa de despliegue (env_file de Compose, secretos, CI). No toques la configuración del daemon de Docker.

Tarea 3: Inspecciona el estado, código de salida y OOMKilled

cr0x@server:~$ docker inspect -f 'ExitCode={{.State.ExitCode}} OOMKilled={{.State.OOMKilled}} Error={{.State.Error}} FinishedAt={{.State.FinishedAt}}' myservice
ExitCode=137 OOMKilled=true Error= FinishedAt=2026-02-04T10:15:19.120401234Z

Significado: Código de salida 137 más OOMKilled=true es una clásica muerte por memoria (SIGKILL).

Decisión: Ve a revisar los logs del kernel y los límites de memoria del contenedor. Esto no es un “bug de la app” hasta que se demuestre lo contrario.

Tarea 4: Confirma kill OOM en los logs del kernel

cr0x@server:~$ sudo dmesg -T | tail -n 20
[Sun Feb  4 10:15:19 2026] Memory cgroup out of memory: Killed process 23184 (myapp) total-vm:812340kB, anon-rss:512120kB, file-rss:1200kB, shmem-rss:0kB
[Sun Feb  4 10:15:19 2026] oom_reaper: reaped process 23184 (myapp), now anon-rss:0kB, file-rss:0kB, shmem-rss:0kB

Significado: El kernel mató el proceso por presión de memoria del cgroup.

Decisión: Aumenta el límite de memoria del contenedor, reduce el uso de memoria o corrige una fuga. También asegúrate de que el host tiene margen; mover el límite sin capacidad es solo trasladar el fallo.

Tarea 5: Comprueba los límites de recursos aplicados al contenedor

cr0x@server:~$ docker inspect -f 'Memory={{.HostConfig.Memory}} MemorySwap={{.HostConfig.MemorySwap}} PidsLimit={{.HostConfig.PidsLimit}}' myservice
Memory=268435456 MemorySwap=268435456 PidsLimit=100

Significado: Límite de 256 MiB de memoria sin swap adicional; ajustado para muchos entornos de ejecución. El límite de PIDs también puede afectar a apps que fork-ean mucho.

Decisión: Si el servicio espera ser más grande, eleva los límites. Si debería ser pequeño, perfila la memoria y elimina picos (JIT warmup, caches, migraciones).

Tarea 6: Identifica si la política de reinicio está forzando el bucle

cr0x@server:~$ docker inspect -f 'Name={{.Name}} RestartPolicy={{.HostConfig.RestartPolicy.Name}} MaximumRetryCount={{.HostConfig.RestartPolicy.MaximumRetryCount}}' myservice
Name=/myservice RestartPolicy=always MaximumRetryCount=0

Significado: “always” significa que se reiniciará incluso si la app sale con 0. MaximumRetryCount=0 significa ilimitado.

Decisión: Durante el debug, considera temporalmente poner on-failure:5 o deshabilitar reinicios para poder inspeccionar el contenedor detenido sin que se regenere de inmediato.

Tarea 7: Para el bucle el tiempo suficiente para inspeccionar de forma segura

cr0x@server:~$ docker update --restart=no myservice
myservice

Significado: Cambiaste la política de reinicio para esta instancia de contenedor.

Decisión: Ahora deténlo y arranca manualmente cuando estés listo. También arregla la fuente (archivo Compose, unidad systemd) o volverá en el siguiente despliegue.

Tarea 8: Inspecciona los eventos del contenedor para ver el ritmo y la causa

cr0x@server:~$ docker events --since 10m --filter container=myservice
2026-02-04T10:15:18.992345678Z container die b5a1c0b7fd4b (exitCode=137, image=myapp:1.4.2, name=myservice)
2026-02-04T10:15:19.101234567Z container start b5a1c0b7fd4b (image=myapp:1.4.2, name=myservice)
2026-02-04T10:15:24.220987654Z container die b5a1c0b7fd4b (exitCode=137, image=myapp:1.4.2, name=myservice)

Significado: Cadencia de reinicio clara. Código de salida repetido.

Decisión: Códigos de salida idénticos repetidos suelen indicar fallo determinista en el arranque (config, permisos, bind de puerto) o kill determinista (OOM en warmup). Enfócate ahí, no en flakiness de red aleatorio.

Tarea 9: Comprueba el estado de salud (los healthchecks pueden crear “bucle suaves de reinicio”)

cr0x@server:~$ docker inspect -f 'Health={{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}} FailingStreak={{if .State.Health}}{{.State.Health.FailingStreak}}{{else}}0{{end}}' myservice
Health=unhealthy FailingStreak=12

Significado: El proceso del contenedor puede estar en ejecución, pero el healthcheck falla repetidamente. Algunas configuraciones (Compose con dependencias, watchdogs externos) responden reiniciando.

Decisión: Inspecciona el comando del healthcheck y su salida a continuación. Trátalo como código de producción, porque lo es.

Tarea 10: Recupera los logs del healthcheck

cr0x@server:~$ docker inspect -f '{{range .State.Health.Log}}{{.End}} {{.ExitCode}} {{.Output}}{{end}}' myservice
2026-02-04T10:16:02.000000000Z 1 curl: (7) Failed to connect to 127.0.0.1 port 8080: Connection refused
2026-02-04T10:16:12.000000000Z 1 curl: (7) Failed to connect to 127.0.0.1 port 8080: Connection refused

Significado: Tu app no está escuchando en el puerto que el healthcheck espera (o está enlazada a otra interfaz, o aún no ha arrancado).

Decisión: Arregla la dirección/puerto de escucha de la app o el healthcheck. Si el arranque es lento, ajusta start_period para evitar fallos prematuros.

Tarea 11: Detecta conflictos de bind de puertos en el host

cr0x@server:~$ sudo ss -ltnp | grep ':8080 '
LISTEN 0      4096         0.0.0.0:8080       0.0.0.0:*    users:(("old-nginx",pid=1187,fd=7))

Significado: Algo más ya está escuchando en ese puerto del host.

Decisión: Cambia el mapeo de puerto publicado o detén el servicio en conflicto. Si esto “funciona en mi laptop”, suele ser porque tu laptop no tenía el daemon conflictivo.

Tarea 12: Valida montajes y permisos (el asesino silencioso)

cr0x@server:~$ docker inspect -f '{{range .Mounts}}{{.Type}} {{.Source}} -> {{.Destination}} (RW={{.RW}}){{"\n"}}{{end}}' myservice
bind /srv/myservice/config.yaml -> /etc/myapp/config.yaml (RW=false)
volume myservice-data -> /var/lib/myapp (RW=true)

Significado: La configuración es un bind mount y está en solo lectura. Eso está bien. Pero si la app intenta escribir en él, fallará.

Decisión: Asegúrate de que la app escribe solo en rutas escribibles. Si necesita generar configuración, monta un directorio y escribe en él, o cambia el comportamiento de la app.

Tarea 13: Entra en un shell de depuración (sin cambiar la imagen)

cr0x@server:~$ docker run --rm -it --network container:myservice --pid container:myservice --entrypoint /bin/sh myapp:1.4.2
/ # ps aux
PID   USER     TIME  COMMAND
1     root      0:00 myapp --config /etc/myapp/config.yaml
/ # netstat -ltn
Active Internet connections (only servers)
tcp        0      0 127.0.0.1:9090          0.0.0.0:*               LISTEN

Significado: Puedes observar procesos y puertos en los namespaces del contenedor. Aquí escucha en 9090, no en 8080.

Decisión: Arregla el healthcheck / el mapeo de puertos. Los namespaces quitan las conjeturas.

Tarea 14: Comprueba la presión del sistema de archivos y el agotamiento de inodos

cr0x@server:~$ df -h /var/lib/docker
Filesystem      Size  Used Avail Use% Mounted on
/dev/nvme0n1p3  100G   98G  2.0G  99% /var/lib/docker
cr0x@server:~$ df -i /var/lib/docker
Filesystem       Inodes   IUsed    IFree IUse% Mounted on
/dev/nvme0n1p3  6553600  6551000     2600  100% /var/lib/docker

Significado: Disco casi lleno y los inodos agotados. Los contenedores pueden fallar de formas extrañas: no pueden escribir archivos PID, no pueden extraer capas, no pueden anexar logs.

Decisión: Limpia imágenes/contendores/volúmenes, amplía el almacenamiento o mueve el root de Docker. Luego vuelve a probar. Si no arreglas inodos, “añadir 10GB” no ayudará.

Tarea 15: Revisa los logs del daemon de Docker (a veces el demonio es el villano)

cr0x@server:~$ sudo journalctl -u docker --since "10 minutes ago" -n 50
Feb 04 10:15:19 server dockerd[1023]: containerd: time="2026-02-04T10:15:19Z" level=warning msg="failed to shim reaping" id=b5a1c0b7fd4b
Feb 04 10:15:19 server dockerd[1023]: Error response from daemon: OCI runtime create failed: runc create failed: unable to start container process: error during container init: error mounting "/srv/myservice/config.yaml" to rootfs at "/etc/myapp/config.yaml": permission denied: unknown

Significado: Errores OCI/runtime pueden impedir que el contenedor siquiera arranque. Esto es distinto de “la app arrancó y se cayó”.

Decisión: Arregla permisos de montaje, perfiles SELinux/AppArmor o la existencia de la ruta en el host. Los ingenieros de aplicación no pueden arreglar lo que no llega a arrancar.

Modos de fallo que causan bucles de reinicio

La mayoría de los bucles de reinicio pertenecen a una de estas categorías. Aprende el patrón y dejarás de tratar cada incidente como un copo de nieve único.

1) La aplicación sale porque la configuración es incorrecta

Firma: ExitCode es un entero pequeño distinto de cero (1, 2, 64), los logs muestran “missing env var”, “invalid config”, “failed to parse”.

Causas típicas: variable de entorno ausente tras rotación de secretos, ruta de archivo de configuración equivocada, bug en plantillas, sintaxis JSON/YAML.

Solución: Valida la configuración en tiempo de build/despliegue. Añade un modo “configtest” en el entrypoint. Fallar rápido está bien, pero falla una vez (limita reintentos durante el rollout).

2) Comportamiento de PID 1 y manejo de señales (el clásico “funciona en local”)

Dentro de un contenedor, el proceso principal es PID 1. PID 1 tiene semánticas especiales en Linux: ignora algunas señales por defecto y es responsable de recolectar zombies. Si envuelves tu app en un script de shell ingenuo, puedes obtener comportamientos extraños al apagar, hijos que nunca mueren, o “sale inmediatamente” porque el script terminó.

Firma: El contenedor sale 0 rápidamente; los logs muestran que el script terminó; no hay proceso de larga ejecución. O el contenedor no se detiene limpiamente y recibe SIGKILL, luego se reinicia.

Solución: Usa exec en los scripts de entrypoint. Considera un init mínimo (como tini) si generas subprocesos.

3) Kills OOM y límites de memoria

Los kills OOM crean los bucles de reinicio más limpios y crueles: todo arranca, ocupa mucha memoria (JVM warmup, importación masiva en Python, paso de build en Node, cache en memoria), luego el kernel lo mata. Docker lo reinicia. Repite.

Firma: ExitCode 137, OOMKilled=true, logs del kernel muestran OOM de cgroup.

Solución: Aumenta límites basados en mediciones, no en esperanza; limita caches; reduce concurrencia; evita hacer migraciones pesadas en cada arranque.

4) Healthchecks demasiado agresivos (o simplemente erróneos)

Los healthchecks son geniales hasta que están escritos como un test unitario: frágiles, dependientes del tiempo y convencidos de que tu servicio está muerto porque una conexión TCP falló una vez.

Firma: El servicio está en ejecución pero se marca como “unhealthy”, el orquestador reinicia o los servicios dependientes se niegan a arrancar.

Solución: Añade start_period, ajusta interval/retries y haz que la comprobación refleje la disponibilidad real al usuario (no la perfección interna).

5) Almacenamiento y sistemas de archivos: disco lleno, permisos erróneos, montajes rotos

Los problemas de almacenamiento no siempre gritan. A veces susurran: “sistema de archivos de solo lectura”, “no hay espacio en el dispositivo”, “permission denied”. Entonces la app sale. Luego Docker la reinicia. Sufrimiento infinito y educado.

Firma: Los logs mencionan fallos de escritura; los logs del daemon muestran errores de montaje; disco/inodos cerca del 100%.

Solución: Arregla montajes, propiedad (UID/GID), etiquetas SELinux y capacidad. Además: deja de escribir logs dentro del filesystem del contenedor como si fuera 2014.

6) Fallos de dependencias: DNS, TLS, bases de datos y orden de arranque

Las apps a menudo asumen que las dependencias están disponibles de inmediato. En sistemas distribuidos, esa suposición es adorable y está equivocada.

Firma: Logs muestran connection refused/timeouts a la BD; exit code distinto de cero; reinicios inmediatos al arrancar.

Solución: Backoff y retry en la aplicación. O usa un patrón de init container (fuera del Docker puro) o un script de inicio que espere con timeouts. Evita bucles “wait-for-it” infinitos sin deadline.

7) “Optimizaciones” que cambian el timing y rompen todo

Cuando aprietas el tiempo de arranque o reduces el tamaño de la imagen, cambias el timing y el entorno de ejecución. Eso puede sacar a la luz condiciones de carrera: el healthcheck corre antes, dependencias no listas, archivos no creados todavía.

Firma: Empieza a oscilar tras una “limpieza” de imagen o un cambio de imagen base; mismo código, comportamiento distinto.

Solución: Trata cambios de base image y entrypoint como cambios de producción. Prueba cold-start. Prueba con límites de recursos realistas.

Broma #2: El contenedor se está “reiniciando para aplicar actualizaciones”, que es exactamente lo que dice justo antes de no hacerlo.

Tres microhistorias corporativas desde producción

Microhistoria 1: El incidente causado por una suposición equivocada

El equipo tenía una pequeña API interna en Docker Compose en un par de VMs. Configuración sencilla: contenedor de la app, contenedor Postgres y un reverse proxy. Había funcionado meses—hasta un parche menor del SO y un redeploy.

Tras el redeploy, el contenedor de la API entró en un bucle de reinicio cerrado. El primer respondededor hizo lo que muchos hacemos bajo presión: culpó a “la red de Docker”. Los logs en la ejecución actual eran escasos: solo banners de inicio. Nada obvio.

Alguien ejecutó docker logs --previous y vio de inmediato una línea sobre fallar al abrir un archivo de certificado. La suposición había sido: “El certificado está incrustado en la imagen.” No lo estaba. Era un bind mount desde el host, y el parche del SO había cambiado los permisos del directorio donde vivía el certificado.

El proceso API corría como un usuario no root. No pudo leer el certificado, así que salió. Docker lo reinició. Bucle infinito.

La solución fue aburrida: corregir la propiedad y permisos en la ruta del host, luego redeploy. La solución duradera fue aún más aburrida: dejar de asumir que los archivos del host son estables; gestionarlos explícitamente (gestión de configuración o un secret store) y añadir una verificación de arranque que imprima un error claro antes de hacer cualquier otra cosa.

Microhistoria 2: La optimización que salió mal

Un equipo de plataforma quería despliegues más rápidos y imágenes más pequeñas. Movieron varios servicios de una imagen basada en Debian a otra más ligera basada en Alpine. Los tiempos de build mejoraron. El output del scan de CVE quedó mejor. Todos se sintieron como si hubieran “reducido desperdicio”.

Una semana después, un servicio comenzó a oscilar tras una release rutinaria. No era consistente en todos los nodos. En algunos nodos corría horas; en otros se reiniciaba cada minuto. La política de reinicio era unless-stopped, así que seguía intentando.

La causa raíz resultó ser una dependencia nativa. El servicio usaba una librería que se comportaba distinto bajo musl (Alpine) que bajo glibc (Debian). Bajo carga, el uso de memoria se disparaba, cruzando el límite del cgroup. El kernel lo mató (exit 137), se reinició y el ciclo se repitió. Como la distribución de carga variaba por nodo, el problema parecía “aleatorio”.

Hicieron rollback de la imagen base para ese servicio y luego el trabajo duro: fijaron límites de memoria realistas, construyeron una prueba de carga apropiada que midiera RSS en steady-state y warmup, y documentaron qué servicios eran seguros de adelgazar.

La lección no fue “Alpine es malo”. La lección fue: las optimizaciones cambian la física. Si no mides, solo estás reorganizando fallos.

Microhistoria 3: La práctica aburrida pero correcta que salvó el día

Un equipo del área financiera ejecutaba un job por lotes en contenedores que producía archivos consumidos por otro sistema. No era glamoroso. Corría una vez por hora, escribía en un volumen montado y salía. La política de reinicio era on-failure:3, no always. Esa elección parecía conservadora, incluso timorata.

Una mañana, el job empezó a fallar inmediatamente. El fallo no estaba en los logs de la aplicación; estaba en los logs del daemon de Docker: una ruta de bind mount no existía en uno de los hosts tras una reorganización del sistema de archivos. El contenedor ni siquiera arrancó.

Porque la política de reinicio estaba acotada, el job se detuvo después de tres intentos en lugar de oscilar durante horas y consumir recursos. Su alerta saltó por “el job no se ejecutó” en lugar de “la CPU del nodo está en llamas”. El ingeniero de guardia pudo diagnosticar sin que el sistema cambiara constantemente bajo sus pies.

Arreglaron la ruta de montaje y añadieron una comprobación previa en el pipeline de despliegue que verificara que las rutas del host existen y tienen permisos correctos. El job volvió a ser aburrido, que es el estado correcto para sistemas adyacentes a finanzas.

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

Esta es la sección que desearás tener durante un incidente. Los síntomas son lo que ves. Las causas raíz son lo que realmente está pasando. Las soluciones son el camino más corto y seguro hacia la estabilidad.

1) “Restarting (0)” para siempre

Síntoma: El contenedor se reinicia, el código de salida aparece como 0.

Causa raíz: Política de reinicio always con un proceso que sale con éxito (script termina; contenedor job no pensado para ser de larga ejecución), o un supervisor que sale después de spawnear un hijo incorrectamente.

Solución: Usa on-failure para jobs one-shot. Asegura que el script de entrypoint use exec para que el servicio real sea PID 1.

2) Exit code 137 y “OOMKilled=true”

Síntoma: Reinicios en un punto consistente del arranque; a veces funciona con menor carga; los logs se cortan.

Causa raíz: Kill OOM del cgroup de memoria.

Solución: Mide memoria; sube límites apropiadamente; arregla fugas; ajusta runtimes (heap de JVM, flags de memoria de Node). Confirma con logs del kernel.

3) “permission denied” en montajes

Síntoma: Logs del daemon muestran errores de permiso en montajes OCI; o logs de app muestran fallos al abrir archivos.

Causa raíz: Permisos del filesystem del host, etiquetas SELinux, perfiles AppArmor, o mapeo de usuarios en Docker rootless.

Solución: Corrige propiedad/permisos; aplica el contexto SELinux correcto; para rootless, asegúrate de que las rutas sean accesibles para el usuario que ejecuta dockerd.

4) Healthcheck falla mientras la app está bien

Síntoma: La app responde en un puerto/ruta, pero el healthcheck marca unhealthy; el orquestador reinicia o mantiene el servicio fuera de rotación.

Causa raíz: Puerto equivocado, ruta incorrecta, desajuste TLS, o arranque más lento que el start time del healthcheck.

Solución: Arregla el comando del healthcheck; añade start_period; verifica que la salud esté ligada al readiness visible al usuario.

5) El contenedor nunca llega a los logs de la app

Síntoma: docker logs está vacío; el contenedor muere instantáneamente; el daemon muestra errores de runtime.

Causa raíz: Imagen/entrypoint faltante, error de formato de exec (arquitectura equivocada), fallo de montaje, binario ausente, usuario inválido.

Solución: Inspecciona logs del daemon; verifica la arquitectura de la imagen; prueba docker run --entrypoint con un shell; valida que los montajes existan.

6) Bucle de reinicio tras “cleanup” o “hardening”

Síntoma: Funciona en dev; falla en prod tras activar root FS readonly, bajar privilegios o eliminar paquetes.

Causa raíz: La app escribe en el filesystem raíz, necesita certificados CA, datos de zona horaria o espera que /tmp sea escribible.

Solución: Proporciona volúmenes escribibles para las rutas necesarias; instala datos de runtime requeridos; documenta ubicaciones de filesystem y permisos necesarios.

7) Reinicios aleatorios correlacionados con la carga

Síntoma: Contenedor estable de noche, oscila en hora pico.

Causa raíz: Picos de memoria que causan OOM, agotamiento de descriptores de archivo, agotamiento de hilos/procesos, o timeouts aguas arriba que provocan crash-on-start.

Solución: Rastrear uso de recursos; establecer ulimits; añadir retropresión; evitar crash-on-transient dependency failures.

Listas de verificación / plan paso a paso

Checklist A: “Mi contenedor se está reiniciando ahora mismo” (plan de 10 minutos)

  1. Identifica el contenedor: docker ps --no-trunc.
  2. Coge los logs anteriores: docker logs --previous <name>.
  3. Inspecciona la razón de salida: docker inspect para ExitCode y OOMKilled.
  4. Si ExitCode=137 o OOMKilled=true: revisa dmesg y límites de memoria.
  5. Si los logs muestran config/env: compara las vars esperadas vs el env real del contenedor y la configuración de despliegue.
  6. Si montaje/permiso: revisa los montajes con docker inspect y los logs del daemon.
  7. Si healthcheck: inspecciona los logs en .State.Health; verifica puerto/ruta.
  8. Para el bucle si está dañando el host: docker update --restart=no y luego docker stop.
  9. Arregla en la fuente (Compose/systemd/CI) para que el siguiente despliegue no reintroduzca el bucle.
  10. Reinicia una vez, observa eventos y logs, confirma estabilidad.

Checklist B: “Hacer los bucles de reinicio menos dolorosos” (controles en diseño)

  1. Usa reintentos acotados cuando corresponda: on-failure:5 para servicios tipo job.
  2. Añade comprobaciones de arranque claras: verifica vars requeridas, archivos y conectividad con errores nítidos.
  3. Haz que los scripts de entrypoint usen exec y salgan con código distinto de cero en fallos fatales de arranque.
  4. Define healthchecks que reflejen la readiness real, con un periodo de gracia al inicio.
  5. Establece límites de recursos realistas y monitorízalos; “ilimitado” no es una estrategia, es una confesión.
  6. Mueve el estado persistente a volúmenes; trata el FS del contenedor como efímero.
  7. Centriza logs; no confíes solo en “docker logs” como tu único registro durante un incidente.
  8. Documenta dependencias y comportamiento ante fallos (¿qué pasa si la BD está caída al arrancar?).

Hechos interesantes y contexto histórico

  • Hecho 1: La popularidad temprana de Docker (circa 2013–2014) se debió al empaquetado y la distribución, no a la orquestación; los bucles de reinicio se hicieron más visibles cuando la gente empezó a tratar contenedores como mascotas.
  • Hecho 2: El estándar OCI runtime existe porque el ecosistema necesitaba comportamiento consistente entre herramientas; muchos “problemas de Docker” son en realidad errores del runtime (runc/containerd) que Docker muestra.
  • Hecho 3: El código de salida 137 típicamente indica SIGKILL (128 + 9). En el mundo de contenedores, eso a menudo se traduce en kills OOM, pero también puede ser un kill externo.
  • Hecho 4: Las semánticas de PID 1 son anteriores a los contenedores; los contenedores solo hacen que más apps accidentalmente se conviertan en PID 1 sin estar diseñadas para ello.
  • Hecho 5: Los healthchecks se añadieron a Docker mucho después de que existiera “docker run”; muchas imágenes aún se distribuyen sin ellos, y muchos equipos los añaden sin ajustar el timing de arranque.
  • Hecho 6: Los drivers de logs (json-file, journald, syslog, fluentd, etc.) afectan lo que “docker logs” puede mostrar; el diagnóstico de reinicios cambia si los logs no se almacenan localmente.
  • Hecho 7: Los sistemas de archivos overlay (overlay2) cambiaron el rendimiento y las semánticas del almacenamiento de contenedores comparado con drivers antiguos; algunos “fallos aleatorios de arranque” del pasado eran casos límite del driver de almacenamiento.
  • Hecho 8: Las políticas de reinicio preceden a los orquestadores modernos; son un mecanismo local de fiabilidad, no una estrategia completa de scheduling. Por eso pueden amplificar problemas en un único host.
  • Hecho 9: El comportamiento de Compose de recrear contenedores (nuevos IDs) vs reiniciar en el mismo lugar es una fuente frecuente de confusión cuando la gente espera que --previous funcione siempre por nombre.

Preguntas frecuentes

1) ¿Cuál es “el único registro que necesito” cuando un contenedor se reinicia para siempre?

Los registros del intento anterior: docker logs --previous <container>. Captura el crash que perdiste mientras mirabas el nuevo arranque.

2) ¿Por qué docker logs no muestra nada útil durante un bucle de reinicio?

Porque estás mirando el momento incorrecto del ciclo de vida. El contenedor puede salir antes de producir salida, o la línea útil se imprimió en el intento previo. Usa --previous e inspecciona el estado de salida.

3) ¿Docker recrea el contenedor o reinicia el mismo?

La política de reinicio de Docker reinicia el mismo contenedor (mismo ID). Algunas herramientas de mayor nivel (Compose en actualizaciones, Swarm al reprogramar) pueden crear un contenedor/tarea nuevo, lo que cambia cómo obtener los logs “anteriores”.

4) ¿Qué significa el código de salida 137 en Docker?

Comúnmente significa que el proceso recibió SIGKILL. En contenedores, eso frecuentemente es el OOM killer del kernel. Confirma con docker inspect (OOMKilled) y dmesg.

5) Mi contenedor sale con código 0 pero aún así se reinicia. ¿Cómo?

La política de reinicio always reiniciará incluso tras una salida exitosa. Eso está bien para daemons, incorrecto para jobs. Cambia a on-failure o rediseña el contenedor para que permanezca en ejecución si es un servicio.

6) ¿Puede un healthcheck fallido causar reinicios?

Docker en sí no reinicia automáticamente por estado unhealthy, pero sistemas externos suelen hacerlo: dependencias de Compose, scripts, unidades systemd o controladores de load balancer. Diagnostica la salida del healthcheck de todos modos; suele señalar el problema real de readiness.

7) ¿Cómo detengo un bucle de reinicio sin borrar todo?

Deshabilita temporalmente la política de reinicio: docker update --restart=no <name>, luego detén el contenedor. Arregla la causa raíz y después vuelve a habilitar la política deseada vía tu configuración de despliegue.

8) ¿Y si no puedo usar docker logs porque los logs se envían a otro sitio?

Entonces el “único registro” es el equivalente en tu pipeline de logging, filtrado por ID de contenedor y marca temporal alrededor del crash. Aún así, docker inspect para códigos de salida y los logs del daemon siguen siendo la verdad local.

9) ¿Cómo depuro un contenedor que muere demasiado rápido para hacer exec?

Deshabilita reinicios, ejecuta la imagen con un entrypoint override (shell), o ejecuta un contenedor de depuración en los mismos namespaces. El objetivo es observar el filesystem, env vars y red desde la misma perspectiva que la app.

10) ¿Es esto lo mismo que Kubernetes CrashLoopBackOff?

Es el mismo modo básico de fallo—el proceso sale y el sistema reintenta—pero Kubernetes añade backoff, eventos, probes y gestión de réplicas. Los primitivos de diagnóstico (logs anteriores, códigos de salida, OOM) siguen aplicando.

Siguientes pasos que deberías hacer de verdad

Si tienes un contenedor que se reinicia para siempre, haz esto en orden:

  1. Ejecuta docker logs --previous y léelo seriamente.
  2. Ejecuta docker inspect para ExitCode y OOMKilled; decide si estás en territorio de “crash” o de “kill”.
  3. Revisa los logs del daemon para problemas OCI/montaje si el contenedor nunca llega a arrancar realmente.
  4. Si es OOM: confirma con dmesg, luego arregla límites/capacidad o el perfil de memoria de la app.
  5. Arregla la fuente de la verdad (archivo Compose, unidad systemd, configuración de CI), no el contenedor en vivo, salvo que hagas un parche de emergencia temporario.

Luego haz las mejoras aburridas: reintentos acotados cuando correspondan, healthchecks ajustados, comportamiento correcto de PID 1 y una comprobación previa de configuración que falle fuerte una vez en lugar de fallar silenciosamente para siempre.

← Anterior
Windows Update atascado: soluciona sin formatear el PC
Siguiente →
Proxmox: Respaldar máquinas virtuales Windows sin dolor de VSS — Un flujo de trabajo práctico

Deja un comentario