El peor tipo de incidente es el que parece haberse solucionado solo. Tu stack “se levanta”, los paneles de control se ponen en verde,
y cinco minutos después la aplicación empieza a devolver 500 porque la base de datos en realidad no estaba lista—solo “iniciada” técnicamente.
Mientras tanto, tu orquestador hizo exactamente lo que le pediste. Esa es la parte que duele.
Docker y Docker Compose son herramientas toscas: pueden arrancar contenedores en un orden determinado, pero no pueden saber mágicamente
cuándo una dependencia es segura de usar a menos que les enseñes qué significa “lista”. La cura es separar el orden de inicio
de la disponibilidad, incorporar comprobaciones basadas en evidencias y hacer que los “falsos inicios” sean imposibles o, al menos, claramente visibles.
Orden de inicio vs disponibilidad: la diferencia que importa
Orden de inicio responde: “¿Se lanzó el proceso?” Eso es todo. Docker puede hacer esto: el contenedor A arranca,
luego el contenedor B arranca. Bien y ordenado. También peligrosamente incompleto.
Disponibilidad responde: “¿El servicio es utilizable por sus clientes ahora mismo?” Eso incluye: sockets escuchando,
migraciones completadas, caches calentadas, claves TLS cargadas, elección de líder realizada, permisos correctos y dependencias accesibles.
La disponibilidad es un contrato entre un servicio y el mundo. Docker no infiere ese contrato. Tú lo implementas.
Los falsos inicios suceden cuando tratamos el orden de inicio como un sustituto de la disponibilidad. Es como declarar un restaurante abierto
porque las luces están encendidas mientras el chef todavía negocia con una caja de papas congeladas.
Un stack fiable hace dos cosas:
- Arranca componentes en un orden sensato cuando eso ayuda.
- Bloquea componentes dependientes hasta verificar la disponibilidad probada, no por optimismo.
A veces oirás “solo añade sleeps.” Eso no es disponibilidad; es superstición con temporizador.
Tu yo futuro te odiará, y tu canal de incidentes te odiará antes.
Por qué ocurren los falsos inicios (y por qué son tan comunes)
“Iniciado” es un estado barato. Un proceso puede iniciarse y seguir siendo inútil. En los sistemas modernos, “útil” a menudo depende de:
alcance de red, estado del sistema de archivos, credenciales, migraciones de esquema y servicios ascendentes. Cualquiera de esos puede retrasarse
respecto al inicio del proceso por segundos o minutos.
Modos de fallo clásicos que generan falsos inicios
- El socket aún no escucha. La aplicación arranca y luego enlaza a un puerto más tarde. Los clientes intentan conectarse de inmediato y fallan.
- El puerto está escuchando, pero el servicio no está listo. HTTP devuelve 503 porque las migraciones o el calentamiento de cache están en curso.
- La base de datos acepta TCP pero no consultas. PostgreSQL acepta conexiones mientras reproduce WAL o ejecuta recuperación.
- El DNS no está asentado. El nombre del contenedor existe, pero cachés/resolvers/sidecars no están listos.
- La dependencia está lista, pero con el esquema incorrecto. La aplicación arranca antes de las migraciones y falla con “relation does not exist.”
- Volumen no montado o permisos incorrectos. El servicio arranca, escribe en ningún lado y luego colapsa bajo sus propias mentiras.
- Limites de tasa y estampida de peticiones. Diez réplicas “arrancan” y todas atacan una dependencia; aprendes lo que significa “retry storm”.
Aquí está la verdad incómoda: muchas aplicaciones están escritas asumiendo que un humano las reiniciará si algo sale mal al arrancar.
Los contenedores quitan al humano y los reemplazan por automatización que reintentará felizmente el mismo fallo para siempre.
Broma #1: Si crees que “depends_on” significa “funciona_con”, alguien te venderá un puente de Docker—y probablemente esté en mantenimiento.
Hechos y contexto histórico (la versión corta y concreta)
- La antigua función “link” de Docker (antes de la madurez de Compose) intentaba cablear dependencias inyectando variables de entorno; no resolvía la disponibilidad.
- El formato de archivo Compose v2 popularizó
depends_onpara orden de inicio; muchos equipos lo leyeron mal como bloqueo de disponibilidad. - Compose v3 cambió el foco hacia Swarm y eliminó algunas semánticas condicionales de arranque; la gente siguió asumiendo que el comportamiento antiguo existía.
- Kubernetes introdujo probes de readiness como concepto de primera clase porque “contenedor en ejecución” nunca fue suficiente para enrutar tráfico.
- systemd ha tenido ordenamiento de dependencias durante años, y aun así distingue “iniciado” de “listo” mediante mecanismos de notificación.
- PostgreSQL puede aceptar conexiones TCP antes de estar completamente disponible para carga de trabajo (recuperación, replay de WAL, checkpoints).
- Variantes de MySQL pueden escuchar temprano pero rechazar autenticación o bloquear tablas internas durante la inicialización, creando la trampa perfecta de falso inicio.
- Se añadieron healthchecks a Docker para ir más allá de “el proceso existe” como única señal; siguen siendo poco usados o mal usados.
Qué hace realmente Docker Compose (y qué no)
depends_on: orden, no disponibilidad
En Compose, depends_on controla el orden de arranque/apagado. Por defecto no espera a que la dependencia esté
lista. Asegura que Docker haya intentado iniciar el contenedor. Eso es todo.
Compose puede usar healthchecks para bloquear el arranque en algunos modos, pero la realidad operativa es desordenada:
la gente usa diferentes versiones de Compose, diferentes motores Docker y expectativas formadas por posts antiguos.
Si quieres fiabilidad, construye la lógica de disponibilidad en tu stack de manera explícita y comprobable.
Healthcheck: útil, pero debes diseñarlo
Un healthcheck es una prueba periódica ejecutada por el motor. Si falla, Docker marca el contenedor como unhealthy. Esa es una señal.
Lo que hagas con ella—reinicios, bloqueo, alertas—es una decisión separada.
Si tu healthcheck es “curl localhost:8080”, pero el servicio devuelve 200 mientras sigue fallando en todas las peticiones externas,
has construido un mentiroso. Los mentirosos pasan las pruebas y fallan a los clientes.
Políticas de reinicio: no son disponibilidad, solo persistencia
Las políticas de reinicio tratan de recuperar caídas. No son una estrategia de dependencias. Si tu app sale porque la BD
no estaba lista, las políticas de reinicio convertirán un breve calentamiento de la BD en un bucle de reinicios. Ese bucle también puede amplificar la carga sobre la BD.
La única señal de disponibilidad que importa: “¿Puede el cliente tener éxito?”
La disponibilidad debe definirse desde la perspectiva del cliente. Si un servicio depende de una base de datos y una cola, “listo” significa
que puede conectar, autenticarse, ejecutar una consulta simple y publicar/consumir un mensaje pequeño—o lo que sea el “trabajo mínimo viable”
para tu sistema.
Una cita para mantenerte honesto, parafraseando una idea de John Allspaw: idea parafraseada: la fiabilidad viene de aprender y de los bucles de retroalimentación, no de pretender que las fallas no ocurrirán.
Patrones de disponibilidad que funcionan en producción
Patrón 1: Pon healthchecks reales en las dependencias
Si ejecutas PostgreSQL, Redis o una API HTTP en un contenedor, dale un healthcheck que refleje la usabilidad real.
Para Postgres, eso puede ser pg_isready más una consulta real si necesitas validar el esquema. Para HTTP, golpea el endpoint
que verifica dependencias, no una ruta estática “OK”.
Patrón 2: Bloquea servicios dependientes por disponibilidad (explícitamente)
Hay tres enfoques comunes de bloqueo:
- Bloqueo en Compose vía estado de salud (cuando tu entorno lo soporta): depends_on + condiciones de health.
- Scripts de espera en el entrypoint dentro del contenedor dependiente: esperar TCP + validación a nivel de app; luego arrancar.
- Reintentos nativos en la aplicación con backoff y jitter: la mejor respuesta a largo plazo, porque funciona en todas partes, no solo en Docker.
El enfoque más duradero es: la app realiza reintentos correctamente y el orquestador tiene healthchecks. Cinturón y tirantes.
Esto es operaciones. Nos vestimos para el clima que tenemos, no para el que merecemos.
Patrón 3: Trata las migraciones como un job de primera clase
Las migraciones de esquema no son un efecto secundario. Trátalas como un paso separado y explícito: un contenedor/job de una sola ejecución que
corre las migraciones y sale con éxito. Solo entonces arranca los contenedores de la app. Esto evita el caos de “cinco apps compiten por migrar el esquema”.
Patrón 4: Usa un endpoint de readiness que verifique dependencias
Para servicios HTTP, expón:
- /healthz (liveness): “¿está el proceso vivo?”
- /readyz (readiness): “¿puedo atender tráfico real?” (BD accesible, cola accesible, configs críticas cargadas)
Incluso en Docker Compose, esto ayuda porque tu healthcheck puede golpear /readyz en lugar de adivinar.
Patrón 5: Limita tus reintentos, y luego falla en alto
Reintentos infinitos pueden ocultar fallos reales. Reintentos acotados con logs claros te dan un retraso controlado al arrancar cuando las cosas van lentas,
pero aún fallan si el mundo está realmente roto.
Patrón 6: Añade jitter y backoff
Si 20 contenedores arrancan a la vez y todos golpean la BD cada 100ms, te causas un DDoS autoinfligido. El backoff y el jitter convierten estampidas en goteos.
Broma #2: “Solo añade un sleep de 30 segundos” es como acabas con un outage de 31 segundos y un postmortem de 3 horas.
Tareas prácticas: comandos, salidas, decisiones (12+)
Estas son las comprobaciones que realmente ejecuto cuando un stack “arrancó” pero la app se comporta como si fuera alérgica a los lunes.
Cada tarea incluye: comando, qué significa la salida y la decisión que tomas.
Tarea 1: Ver estado y salud de contenedores de un vistazo
cr0x@server:~$ docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}'
NAMES STATUS PORTS
app Up 18 seconds (health: starting) 0.0.0.0:8080->8080/tcp
db Up 22 seconds (healthy) 5432/tcp
redis Up 21 seconds (healthy) 6379/tcp
Significado: app está en ejecución pero no lista; DB y Redis están saludables.
Decisión: No enrutar tráfico todavía. Si la salud de la app permanece en “starting” demasiado tiempo, inspecciona su healthcheck y logs de arranque.
Tarea 2: Inspeccionar por qué falla un healthcheck
cr0x@server:~$ docker inspect --format '{{json .State.Health}}' app
{"Status":"unhealthy","FailingStreak":3,"Log":[{"Start":"2026-01-03T10:10:01.123Z","End":"2026-01-03T10:10:01.456Z","ExitCode":1,"Output":"curl: (7) Failed to connect to localhost port 8080: Connection refused\n"}]}
Significado: La app no está escuchando todavía, o está escuchando en otro puerto/interfaz.
Decisión: Revisa los logs de la app y su dirección/puerto de escucha. Si enlaza a 127.0.0.1 dentro del contenedor, eso está bien para el healthcheck pero no para clientes externos a menos que esté publicado correctamente.
Tarea 3: Leer logs de arranque con timestamps
cr0x@server:~$ docker logs --since 10m --timestamps app | tail -n 30
2026-01-03T10:09:44.001234567Z boot: loading config
2026-01-03T10:09:44.889012345Z db: connection failed: dial tcp db:5432: connect: connection refused
2026-01-03T10:09:45.889045678Z db: connection failed: dial tcp db:5432: connect: connection refused
2026-01-03T10:09:46.889078901Z boot: giving up after 2 retries
Significado: La app lo intentó dos veces y salió demasiado pronto. La BD no estaba lista en ese momento.
Decisión: Aumentar reintentos/backoff o bloquear el arranque hasta verificar la disponibilidad de la BD. También evaluar si “salir si falta BD” es el comportamiento correcto.
Tarea 4: Confirmar que los logs del DB indican disponibilidad real, no solo arranque
cr0x@server:~$ docker logs --since 10m --timestamps db | tail -n 30
2026-01-03T10:09:30.100000000Z PostgreSQL init process complete; ready for start up.
2026-01-03T10:09:31.200000000Z database system is ready to accept connections
Significado: La BD declaró disponibilidad a las 10:09:31; la app falló a las 10:09:44. Esa discrepancia sugiere o hostname equivocado, o problema de red, o que la BD se reinició.
Decisión: Revisa la red entre contenedores y reinicios de la BD; verifica que la app use el nombre de servicio y puerto correctos.
Tarea 5: Verificar bucles de reinicio de contenedores
cr0x@server:~$ docker ps -a --format 'table {{.Names}}\t{{.Status}}\t{{.RunningFor}}'
NAMES STATUS RUNNING FOR
app Restarting (1) 3 seconds 2 minutes
db Up 2 minutes (healthy) 2 minutes
Significado: La app se está crasheando y reiniciando.
Decisión: Deja de intentar “arreglarlo” reiniciando. Inspecciona el código de salida y el error; luego corrige la disponibilidad o la configuración.
Tarea 6: Obtener el código de salida y la última razón de fallo de la app
cr0x@server:~$ docker inspect --format 'ExitCode={{.State.ExitCode}} Error={{.State.Error}} FinishedAt={{.State.FinishedAt}}' app
ExitCode=1 Error= FinishedAt=2026-01-03T10:10:02.002002002Z
Significado: Salida no nula sin error a nivel de motor; el proceso decidió salir.
Decisión: Trátalo como un problema de comportamiento de la aplicación (manejo de dependencias, validación de configuración), no de Docker.
Tarea 7: Confirmar descubrimiento de servicios (DNS) dentro de la red
cr0x@server:~$ docker exec -it app getent hosts db
172.20.0.3 db
Significado: El DNS para db se resuelve correctamente dentro del contenedor.
Decisión: Pasa a comprobar conectividad y autenticación; el DNS no es tu cuello de botella hoy.
Tarea 8: Probar conectividad TCP a una dependencia desde el contenedor dependiente
cr0x@server:~$ docker exec -it app bash -lc 'nc -vz db 5432'
nc: connect to db (172.20.0.3) port 5432 (tcp) failed: Connection refused
Significado: La BD es alcanzable por IP pero no acepta TCP. O bien la BD no está escuchando, está en otro puerto/interfaz, o se está reiniciando.
Decisión: Inspecciona sockets de escucha de la BD e historial de reinicios; también verifica si la BD está ligada solo a localhost.
Tarea 9: Confirmar que la BD escucha en el puerto esperado dentro de su contenedor
cr0x@server:~$ docker exec -it db bash -lc 'ss -lntp | grep 5432 || true'
LISTEN 0 244 0.0.0.0:5432 0.0.0.0:* users:(("postgres",pid=1,fd=7))
Significado: PostgreSQL está escuchando en todas las interfaces.
Decisión: Si los clientes aún ven connection refused, sospecha de timing (BD reiniciándose) o políticas de red/iptables.
Tarea 10: Verificar comando de healthcheck y parámetros de temporización
cr0x@server:~$ docker inspect --format '{{json .Config.Healthcheck}}' db
{"Test":["CMD-SHELL","pg_isready -U postgres -h 127.0.0.1 -p 5432"],"Interval":3000000000,"Timeout":1000000000,"Retries":3,"StartPeriod":0}
Significado: Intervalo 3s, timeout 1s, reintentos 3, y StartPeriod es 0. En discos lentos o recuperación tras crash, eso puede marcar la BD como unhealthy prematuramente.
Decisión: Añadir un periodo de inicio (por ejemplo, 30–60s) y aumentar el timeout. Los healthchecks deben detectar fallos reales, no el calentamiento normal.
Tarea 11: Detectar almacenamiento lento que retrasa la disponibilidad
cr0x@server:~$ docker exec -it db bash -lc 'dd if=/var/lib/postgresql/data/pg_wal/000000010000000000000001 of=/dev/null bs=4M count=16 status=none; echo $?'
0
Significado: Lectura básica exitosa. Esto no prueba rendimiento, pero descarta errores obvios de I/O.
Decisión: Si el arranque sigue lento, revisa saturación de I/O a nivel host y latencia del sistema de archivos; los retrasos de disponibilidad a menudo se rastrean hasta el almacenamiento.
Tarea 12: Comprobar presión de recursos del host (CPU, memoria, I/O) que prolonga el calentamiento
cr0x@server:~$ docker stats --no-stream
CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O
a1b2c3d4e5f6 app 180.12% 512MiB / 1GiB 50.00% 1.2MB / 800KB 12MB / 2MB
b2c3d4e5f6g7 db 95.33% 1.8GiB / 2GiB 90.00% 900KB / 1.1MB 2.3GB / 1.9GB
Significado: La BD está cerca del límite de memoria y con alto I/O de bloque. Eso es receta para disponibilidad lenta y fallos intermitentes.
Decisión: Aumentar memoria, reducir shared buffers, mover volúmenes a almacenamiento más rápido o reducir el paralelismo de inicio. Arregla el cuello de botella antes de “afinar” healthchecks para que mientan.
Tarea 13: Ver gráfico de dependencias de Compose y configuración resuelta
cr0x@server:~$ docker compose config
services:
app:
depends_on:
db:
condition: service_healthy
environment:
DATABASE_URL: postgres://postgres:postgres@db:5432/app
db:
healthcheck:
test:
- CMD-SHELL
- pg_isready -U postgres -h 127.0.0.1 -p 5432
Significado: El bloqueo previsto está presente en la salida de configuración (bien), asumiendo que tu implementación de Compose lo respeta.
Decisión: Si el comportamiento contradice la configuración, verifica la versión de Compose y el motor; luego considera mover el bloqueo al entrypoint o a reintentos en la app para portabilidad.
Tarea 14: Confirmar versiones de Compose y Docker (el comportamiento depende de esto)
cr0x@server:~$ docker version --format 'Client={{.Client.Version}} Server={{.Server.Version}}'
Client=27.2.0 Server=27.2.0
Significado: Sabes en qué runtime estás realmente. Esto importa cuando alguien jura “antes funcionaba”.
Decisión: Alinea versiones entre entornos o incorpora la disponibilidad en la aplicación para que tu stack no dependa de peculiaridades de la herramienta.
Tarea 15: Cronometrar la ruta hasta estar listo
cr0x@server:~$ time docker compose up -d
[+] Running 3/3
✔ Container db Started
✔ Container redis Started
✔ Container app Started
real 0m2.114s
user 0m0.082s
sys 0m0.061s
Significado: Compose informa “Started” rápido. Eso no significa listo.
Decisión: Mide la disponibilidad por separado (estado de salud, endpoints ready, comprobaciones sintéticas). No trates la salida del orquestador como la verdad.
Tarea 16: Ejecutar una comprobación sintética “¿puedo hacer trabajo real?”
cr0x@server:~$ docker exec -it app bash -lc 'curl -fsS http://127.0.0.1:8080/readyz && echo READY'
READY
Significado: La app declara estar lista para tráfico real (asumiendo que tu /readyz es honesto).
Decisión: Solo ahora es razonable poner el servicio detrás de un balanceador, abrir reglas de firewall o declarar la implementación completada.
Guía rápida de diagnóstico
Cuando “los contenedores están arriba” pero el sistema no es usable, la velocidad importa. El truco es evitar adivinar qué dependencia está mintiendo.
Aquí está el orden que encuentra el cuello de botella rápido, con mínima sacudida.
Primero: identifica qué componente no está listo (no cuál está “caído”)
- Ejecuta
docker psy busca(health: starting)o(unhealthy). - Si no existen healthchecks, ese es tu primer problema. Pero aún diagnosticas con logs y comprobaciones sintéticas.
Segundo: correlaciona timestamps entre logs
- Captura los últimos 5–10 minutos de logs con timestamps de la app y las dependencias.
- Busca: connection refused, timeouts, fallos de auth, errores de migración, errores de disco.
- Decide si es un problema de temporización (dependencia calentándose) o una mala configuración (hostname/credenciales incorrectas).
Tercero: prueba desde el namespace de red del cliente
- Desde dentro del contenedor dependiente, prueba DNS, luego TCP, luego protocolo a nivel de app.
- No pruebes desde el host y asumas que es equivalente. Camino de red distinto, verdad distinta.
Cuarto: revisa presión de recursos y latencia de almacenamiento
- Usa
docker statspara detectar saturación de CPU/memoria/disco. - Si la BD tarda en estar lista, sospecha primero del I/O. Las apps son impacientes; los discos son eternos.
Quinto: decide qué capa debe asumir la corrección
- Correción en la app: reintentos con backoff/jitter; endpoint de readiness; mejor manejo de errores.
- Correción en Compose: healthchecks; bloqueo; orden de jobs de migración.
- Correción en la plataforma: rendimiento de almacenamiento; límites de recursos; evitar vecinos ruidosos.
Tres micro-historias del mundo corporativo
Incidente causado por una suposición equivocada: “Iniciado” significaba “listo”
Una empresa mediana ejecutaba un stack de Docker Compose para un portal interno de facturación: web, servicio API, PostgreSQL y un
worker en background. Los despliegues eran “simples”: tirar imágenes, docker compose up -d, listo. Durante meses pareció funcionar.
Entonces un reinicio de host rutinario se convirtió en un incidente de medio día.
Tras el reinicio, Compose arrancó contenedores en el orden esperado. El contenedor de la API arrancó, intentó ejecutar una migración al inicio
y falló inmediatamente porque Postgres todavía estaba reproduciendo WAL. La API salió con código no nulo. La política de reinicio la trajo de vuelta.
Falló otra vez. Y otra. La migración nunca se ejecutó, la API nunca permaneció el tiempo suficiente para aceptar peticiones y el worker
machacó la cola de mensajes con reintentos sin limitación de tasa.
El panel decía “contenedores en ejecución” porque el contenedor de la base de datos estaba arriba y los demás se reiniciaban constantemente.
El ingeniero de guardia inicialmente se centró en el balanceador porque los usuarios veían 502s. Distracción clásica.
Solo después de comparar timestamps entre logs cayó la ficha: la API tenía una dependencia de arranque en un estado de la BD que no estaba garantizado.
La solución fue aburrida y decisiva: las migraciones se movieron a un contenedor de una sola ejecución que corría después de que la BD estuviera healthy, y la API
ganó backoff exponencial en conexión a la BD. También añadieron un endpoint de readiness que fallaba hasta que las migraciones terminaran.
El siguiente reinicio fue un no-evento. El equipo de facturación no envió flores, pero tampoco correos furiosos, que es el equivalente SRE.
Optimización que salió mal: healthchecks “ajustados” para mentir
Otra organización tenía un entorno de desarrollo basado en Compose que se parecía a producción: microservicios, una BD central y un motor de búsqueda.
Los desarrolladores se quejaban de que levantar el stack tardaba demasiado, especialmente en portátiles. Alguien “optimizó” haciendo los healthchecks
muy agresivos: intervalos de un segundo, timeouts de un segundo y sin periodo de inicio. Hacía que la UI mostrara “healthy” más rápido en un buen día.
En un día mediocre—como cuando el motor de búsqueda necesitaba más tiempo para inicializar índices—el contenedor fue marcado unhealthy temprano.
Un script wrapper interpretó unhealthy como “roto”, lo mató y reinició. Eso creó un bucle donde el servicio nunca tuvo tiempo ininterrumpido suficiente para terminar la inicialización.
La optimización no redujo el tiempo de arranque; impidió el arranque por completo.
El equipo entonces persiguió fantasmas: DNS, puertos, ajustes de heap de Java. Todo menos lo obvio: su propia política de healthcheck estaba saboteando activamente el sistema.
Habían construido una prueba de disponibilidad que castigaba la inicialización normal.
La resolución final: se añadieron periodos de inicio, se aumentaron timeouts y el healthcheck para el motor de búsqueda pasó de “puerto abierto” a “estado del clúster yellow/green”
(una señal de que la inicialización superó un umbral significativo). El arranque tardó un poco más. También arrancó siempre. Así es como “más rápido” se ve en producción: menos reintentos, menos bucles, menos mentiras.
Práctica aburrida pero correcta que salvó el día: una puerta sintética de “ready” en CI
Una empresa regulada ejecutaba tests de integración nocturnos contra un entorno Compose. Tenían una costumbre que parecía dolorosamente aburrida:
cada pipeline tenía una etapa explícita de “esperar disponibilidad” usando un script pequeño que consultaba endpoints de readiness y ejecutaba una consulta mínima a la BD.
Solo cuando eso pasaba empezaban las pruebas. Los ingenieros a veces se quejaban por el minuto extra.
Luego llegó una actualización de la imagen de la base de datos. La nueva imagen hacía un paso extra de inicialización cuando detectaba cierta condición del sistema de archivos.
En algunos runners, ese paso tomó tanto tiempo que los contenedores de la app arrancaron y fallaron inmediatamente en su conexión inicial a la BD.
Sin bloqueo, las pruebas habrían comenzado durante el flapping y producido fallos aleatorios.
En su lugar, el pipeline simplemente esperó. Cuando la disponibilidad no llegó dentro del plazo configurado, falló claro con:
“BD no lista después de N segundos.” No pruebas inestables. No artefactos medio rotos. El equipo revirtió la imagen y abrió un issue interno para fijar versiones hasta entender el nuevo comportamiento.
La práctica aburrida dio resultado: la falla fue determinista, localizada y rápida de diagnosticar. Ese es el sueño.
También por eso insisto: trata la disponibilidad como una señal de primera clase, no como una vibra.
Errores comunes: síntoma → causa raíz → solución
1) “Connection refused” al arrancar, luego funciona tras reiniciar manualmente
Síntoma: la app falla inmediatamente con connection refused a BD/Redis, luego funciona si reinicias el contenedor de la app más tarde.
Causa raíz: la dependencia se inició pero aún no estaba escuchando; la app no tiene reintentos o ventana de reintentos insuficiente.
Solución: añade reintentos exponenciales en la app; añade una puerta de disponibilidad (healthcheck + bloqueo o entrypoint wait) para dependencias.
2) El contenedor de la app está “Up”, pero todas las peticiones fallan
Síntoma: estado del contenedor en ejecución; healthcheck en verde; los usuarios ven 500s.
Causa raíz: el healthcheck solo prueba liveness (puerto abierto) y no readiness (éxito de dependencias).
Solución: implementa /readyz que verifique dependencias críticas; apunta el healthcheck del contenedor a él.
3) “Unhealthy” durante el calentamiento normal, causando bucles de reinicio
Síntoma: el servicio nunca se estabiliza; los logs muestran pasos de arranque repitiéndose.
Causa raíz: start period del healthcheck demasiado corto o ausente; script wrapper reinicia en unhealthy.
Solución: configura start_period y timeout razonables; reinicia solo al crash, no por unhealthy temprano, salvo que lo quieras realmente.
4) Fallos aleatorios que desaparecen si serializas el arranque
Síntoma: arrancar servicios uno a uno funciona; arrancarlos juntos falla de forma intermitente.
Causa raíz: estampida sobre una dependencia compartida (BD, servicio de auth, proveedor de secretos) más reintentos agresivos.
Solución: añade jitter/backoff; escalona el arranque; aumenta capacidad de la dependencia; ejecuta migraciones por separado.
5) “Autenticación fallida” al arrancar, luego bien más tarde
Síntoma: fallos transitorios de auth para BD o claves API justo después del arranque.
Causa raíz: sidecar/agent de inyección de secretos no listo; secretos basados en archivos no escritos aún; token IAM no disponible.
Solución: la disponibilidad debe incluir “credenciales presentes y válidas”; bloquea el arranque en esa condición, no solo en el inicio del proceso.
6) La app dice lista, pero las migraciones aún se ejecutan
Síntoma: el endpoint de readiness devuelve OK mientras los cambios de esquema están en curso; los clientes obtienen errores SQL.
Causa raíz: la app no trata las migraciones como dependencia de disponibilidad, o las migraciones se ejecutan en paralelo entre réplicas.
Solución: mueve migraciones a un job dedicado; la disponibilidad de la app debe fallar hasta que la versión de esquema sea compatible.
7) “Funciona en mi máquina”, falla en un host más lento
Síntoma: portátiles de dev bien; runners de CI o VMs pequeñas fallan al arrancar.
Causa raíz: suposiciones de temporización integradas en el arranque; sin backoff; healthchecks demasiado estrictos; almacenamiento más lento.
Solución: aumenta ventanas de tolerancia; mide tiempo hasta estar listo; arregla la dependencia más lenta en lugar de ocultarla.
8) Contenedores muestran healthy, pero la ruta de red para clientes reales está rota
Síntoma: healthchecks internos en verde; clientes externos hacen timeouts.
Causa raíz: el healthcheck solo prueba localhost; el servicio enlaza la interfaz equivocada; publicación de puertos/ingress mal configurada.
Solución: prueba la disponibilidad por el mismo camino que usan los clientes, o incluye una comprobación sintética secundaria desde fuera de la red de contenedores.
Listas de verificación / plan paso a paso
Plan paso a paso: arreglar falsos inicios en un stack Compose existente
-
Inventaria las dependencias. Para cada servicio, anota lo que realmente requiere para servir tráfico:
conectividad a BD, conectividad a cola, cache, secretos, sistema de archivos escribible, versión de esquema. -
Añade un endpoint de disponibilidad (o equivalente) a cada servicio de app. Si no es HTTP, implementa una pequeña comprobación CLI
que realice una operación mínima real (p. ej., una consulta a la BD). - Define Docker healthchecks que reflejen la disponibilidad. No hagas curl a un endpoint de vanidad; haz curl al que comprueba dependencias.
-
Configura tiempos de healthcheck de forma realista. Añade
start_periodpara calentamientos conocidos, usa timeouts que coincidan con tu arranque más lento normal. - Elige estrategia de bloqueo. Si tu entorno soporta bloqueo de Compose en health, úsalo. Si no, bloquea en entrypoint o en la lógica de la app.
- Implementa reintentos con backoff y jitter en las apps. Esto es innegociable para sistemas que deben sobrevivir reinicios y despliegues.
- Separa migraciones en un job dedicado. Ejecútalo una vez, con lock, y falla en alto si no puede completarse.
- Añade una comprobación sintética de “stack listo”. Algo que verifique el flujo de usuario: login, obtención de datos, escritura de un registro.
- Mide tiempo hasta estar listo. Captura timestamps en logs y sigue medianas/p95 de tiempo de arranque; afina healthchecks con datos.
- Prueba el peor día. Reinicia el host, limita CPU y simula disco lento. Si tu modelo de disponibilidad sobrevive eso, sobrevivirá al martes.
Lista de verificación: cómo se ve “bien”
- Cada servicio tiene una señal de disponibilidad significativa.
- Cada dependencia tiene un healthcheck que coincide con la usabilidad.
- Ningún servicio sale al primer fallo de conexión; los reintentos están acotados y logueados.
- Las migraciones se ejecutan una vez, explícitamente, no como efecto secundario del “arranque de la app”.
- Los healthchecks toleran el calentamiento normal; detectan deadlocks reales y malas configuraciones.
- El stack tiene al menos una comprobación sintética end-to-end usada en CI y/o despliegue.
Lista de verificación: qué evitar (porque siempre termina mordiéndote)
- Sleeps hardcodeados como “gestión de dependencias”.
- Healthchecks que solo verifican “puerto abierto”.
- Bucles de reinicio como estrategia de recuperación por defecto.
- Múltiples réplicas compitiendo por migraciones al arrancar.
- Timeouts ajustados para tu portátil más rápido en vez de tu entorno normal más lento.
Preguntas frecuentes
1) ¿No es depends_on suficiente para la mayoría de stacks?
No. Aborda la secuencia de inicio de procesos, no la usabilidad del servicio. Aún tendrás carreras en discos lentos, tras crashes
o cuando las dependencias hacen recuperación interna.
2) ¿Debería bloquear el inicio en Compose o dentro de la app?
Dentro de la app es más portable y más correcto. El bloqueo en Compose es una capa útil, pero tu aplicación correrá en más lugares
que Compose: diferentes hosts, CI, Kubernetes, systemd, quizá bare metal. Reintentar dependencias es responsabilidad de la aplicación.
3) ¿Cuál es la diferencia entre liveness y readiness?
Liveness significa “el proceso está vivo”. Readiness significa “el servicio puede hacer su trabajo para los clientes”. No son intercambiables.
Si las confundes, o matas servicios saludables pero ocupados, o enrutas tráfico a servicios rotos pero en ejecución.
4) Si mi servicio reintenta para siempre, ¿no es eso fiable?
Es resiliente pero no necesariamente fiable. Los reintentos infinitos pueden ocultar outages y crear carga sostenida sobre dependencias.
Usa backoff, jitter y un tiempo máximo de espera con reportes de error claros.
5) ¿Qué es un “falso inicio” en este contexto?
Un falso inicio es cuando el orquestador informa que los servicios arrancaron (o incluso están healthy) pero el sistema no puede servir tráfico correcto.
A menudo se resuelve “solo”, lo que hace fácil ignorarlo hasta que te quema en producción.
6) ¿Cómo escribo un buen healthcheck para una base de datos?
Prefiere una herramienta de disponibilidad nativa de la base de datos (como pg_isready) y, cuando sea necesario, una consulta mínima que ejercite autenticación
y la base de datos correcta. Ten cuidado: una consulta puede ser costosa si se ejecuta con demasiada frecuencia.
7) ¿Por qué mis contenedores arrancan bien en dev pero fallan en CI?
Los runners de CI suelen ser más lentos, más contendedores y más variables. Las suposiciones de tiempo colapsan allí primero. Añade start periods,
usa endpoints de readiness y mide tiempo hasta estar listo en ambos entornos.
8) ¿Es suficiente una comprobación TCP (nc) para la disponibilidad?
Es un buen primer paso, pero no la meta. TCP abierto significa que existe un socket; no significa que la auth funcione, que el esquema sea correcto
o que el servicio no devuelva errores.
9) ¿Pueden los healthchecks perjudicar el rendimiento?
Sí. Un healthcheck frecuente y pesado (como una consulta SQL lenta) puede convertirse en carga auto-infligida. Mantén las comprobaciones ligeras, reduce la frecuencia
y usa periodos de inicio en lugar de sondeos agresivos.
10) ¿Cuál es la mejora más simple con mayor beneficio?
Añade un endpoint real de availability a la app y apunta el healthcheck a él. Luego implementa reintentos con backoff para BD y colas.
Esos dos cambios eliminan la mayoría de la inestabilidad en el arranque.
Conclusión: próximos pasos que puedes hacer esta semana
Trata “iniciado” como un evento mecánico, no como una condición de éxito. Si quieres menos incidentes, deja de pedirle a Docker que infiera la disponibilidad
y empieza a proporcionarle señales que coincidan con la usabilidad real.
Pasos prácticos:
- Añade endpoints de disponibilidad (o comprobaciones mínimas de operación real) a tus servicios de apps.
- Mejora los healthchecks para probar disponibilidad, no solo “puerto abierto”, y ajusta start periods para que reflejen la realidad.
- Implementa reintentos a nivel de app con backoff y jitter para cada dependencia externa.
- Separa las migraciones de esquema en un paso dedicado de una sola ejecución con semántica clara de éxito/fallo.
- Adopta la guía rápida de diagnóstico y convierte los pasos en memoria muscular: salud, logs, comprobaciones en contenedor y luego presión de recursos.
Los falsos inicios no son mala suerte. Son una brecha de diseño. Ciérrala y tu era de “funciona después de reiniciarlo” puede finalmente terminar.