Levantás un docker-compose up con un backend y una base Postgres. El backend tiene depends_on: [db]. Arranca todo, el backend tira ECONNREFUSED a los dos segundos, el contenedor de Postgres sigue vivo, y googleás el error pensando que es un problema de red entre servicios. No lo es. Me pasó armando un stack chico para probar una migración: el contenedor de db estaba "up" en el docker ps, pero Postgres todavía estaba corriendo su inicialización interna y no aceptaba conexiones. depends_on sin más hizo exactamente lo que promete: esperó a que el contenedor de db existiera y estuviera corriendo. Nunca prometió que Postgres adentro estuviera aceptando conexiones.
Ese malentendido es viejo y lo escribí hace poco desde otro ángulo — la diferencia entre lo que un healthcheck le dice a Docker y lo que le dice a un orquestador como Kubernetes. Este post baja un nivel más: al docker-compose.yml que cualquiera edita a diario, sin cluster, sin readiness probes de Kubernetes de por medio.
Docker Compose healthcheck: qué resuelve y qué no
Mi tesis es simple: depends_on sin condition es una ilusión de orden. Te da secuencia de arranque de contenedores, no secuencia de disponibilidad de servicio. Con condition: service_healthy recién ahí tenés algo verificable — Compose no arranca el segundo servicio hasta que el healthcheck del primero reporte healthy.
Lo incómodo es que esta confusión no es un error de configuración raro: es el comportamiento default de Compose, y por eso aparece en tantos repos que "andan bien" hasta que dejan de andar.
La fuente es la especificación oficial de Compose. Ahí queda claro que healthcheck define un comando que Docker ejecuta periódicamente dentro del contenedor, y que depends_on acepta un objeto con condition en vez de solo una lista de nombres de servicios. Las condiciones válidas son service_started, service_healthy y service_completed_successfully. Sin condition explícita, el default es service_started, que es exactamente el comportamiento que rompe expectativas: contenedor arriba, no necesariamente el proceso adentro respondiendo.
Dónde se equivoca la gente
La receta común que veo en repos y en tutoriales es esta:
services:
db:
image: postgres:16
api:
build: .
depends_on:
- db
Compila, arranca, "funciona" en la demo porque Postgres suele levantar rápido en una laptop con SSD. El costo oculto aparece en CI, en una máquina más lenta, o cuando alguien le agrega un entrypoint.sh a la imagen de Postgres que corre migraciones antes de aceptar conexiones. Ahí el margen que "funcionaba por suerte" desaparece y el api falla la primera conexión.
El contraejemplo que corrige esto:
services:
db:
image: postgres:16
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 3s
retries: 5
api:
build: .
depends_on:
db:
condition: service_healthy
Ahora api no arranca hasta que pg_isready devuelva éxito varias veces según interval y retries. Es la diferencia entre "el contenedor existe" y "el contenedor está listo", y es exactamente el mismo concepto de readiness probe que Kubernetes formaliza con readinessProbe — pero acá resuelto a nivel de Compose local, sin necesidad de un cluster.
flowchart LR
A[db arranca] --> B{healthcheck de db}
B -->|starting/unhealthy| B
B -->|healthy| C[api arranca]
C --> D{healthcheck de api}
D -->|healthy| E[stack listo]
Matriz de decisión: cuándo condicionar y cuándo no
No todo servicio necesita esta rigidez. Antes de agregar healthcheck y condition: service_healthy en cada línea, conviene mirar esto:
| Situación | Qué hacer | Por qué |
|---|---|---|
| Servicio con estado (DB, cache, broker) al que otro se conecta al boot |
healthcheck + condition: service_healthy
|
El fallo de conexión temprana es predecible y barato de evitar |
| Servicio sin estado que solo expone HTTP y tolera reintentos en el cliente |
depends_on simple o ninguno |
El costo de esperar puede ser mayor que el de reintentar en la app |
| Job de migración que corre una vez y termina | condition: service_completed_successfully |
No es "healthy" lo que importa, es que terminó bien |
| Entorno de CI con recursos compartidos y arranques lentos |
healthcheck con retries generosos |
Un timeout corto en CI genera falsos negativos que no ves en local |
| Microservicio que ya maneja reconexión con backoff en su propio código |
depends_on simple, dejar que la app reintente |
Duplicar la lógica de espera en Compose y en el código es redundante |
Qué NO se puede concluir de esto
La documentación de Compose no dice cuánto tarda en converger un healthcheck con valores específicos de interval, timeout y retries en producción real, porque eso depende de la imagen, del host y de la carga — no hay un número universal que valga para todos los casos. Tampoco resuelve el problema de "servicio healthy pero todavía no puede atender tráfico real" bajo carga, que es distinto de "arrancó bien en frío". Y depends_on con condition, aunque ordena el arranque, no es un mecanismo de reintento continuo durante la vida del contenedor: si db se cae después de que api ya arrancó healthy, Compose no vuelve a bloquear nada. Para eso hace falta la lógica de reconexión en el código de la app, no en el docker-compose.yml.
Tampoco es un sustituto de readinessProbe y livenessProbe de Kubernetes si el destino final es un cluster: Compose resuelve el problema en desarrollo local o en un docker-compose up de staging, pero la garantía de orquestación productiva vive en otro nivel, con otras herramientas y otras semánticas de reinicio.
FAQ
¿depends_on sin condition sirve para algo?
Sirve para el orden de arranque y para que Compose pare los contenedores en el orden inverso al bajar el stack. No sirve para garantizar que el servicio dependido esté listo para recibir tráfico.
¿Qué pasa si el healthcheck nunca llega a healthy?
El servicio que depende de él con condition: service_healthy no arranca, y Compose lo reporta como fallo de dependencia. Prefiero eso mil veces a un arranque silencioso que falla en runtime y te hace perder media hora googleando un ECONNREFUSED.
¿service_healthy funciona con cualquier imagen?
Solo si la imagen define un healthcheck — propio en el Dockerfile o declarado en el docker-compose.yml. Sin healthcheck definido, Compose no tiene manera de evaluar la condición.
¿Esto reemplaza los readiness probes de Kubernetes?
No. Son conceptos análogos pero viven en capas distintas. healthcheck de Compose es para el mundo de docker-compose up local o self-hosted simple; readinessProbe es la pieza equivalente cuando el destino es un cluster de Kubernetes.
¿Cuántos retries o qué interval conviene poner?
No hay un número que sirva para todos los casos: depende de la imagen y el entorno. Lo prudente es arrancar con valores conservadores, medir en el propio pipeline de CI o entorno de staging, y ajustar según lo que se observe ahí — no copiar un valor de un ejemplo genérico.
¿condition: service_completed_successfully es lo mismo que service_healthy?
No. service_completed_successfully espera a que el contenedor termine con código de salida cero, útil para jobs de migración one-shot. service_healthy espera a un healthcheck que sigue corriendo mientras el contenedor vive.
Postura final
Si tu docker-compose.yml tiene depends_on como lista simple contra un servicio con estado, hay una falla de diseño esperando el momento menos oportuno para aparecer — probablemente en CI, con recursos más ajustados que en tu laptop. La corrección no es compleja: un healthcheck bien definido y condition: service_healthy. Lo que sí pido es que no lo confundas con una garantía de disponibilidad continua ni con el equivalente de un readiness probe de Kubernetes en producción. Es una garantía de arranque ordenado en un entorno local o de staging simple, y como tal hay que tratarla: útil, verificable contra la fuente oficial, y con límites que conviene conocer antes de asumir más de lo que da. Mi límite personal: si el proyecto ya tiene un cluster de verdad, dejo de pelear con Compose y muevo la garantía de readiness a donde corresponde.
Si el stack ya usa TanStack Query contra Server Actions, vale la pena revisar cómo se maneja la invalidación de cache en ese post sobre setQueryData. Y si el próximo paso es Next.js con cache en capas, la diferencia entre revalidatePath y revalidateTag está detallada en este otro análisis.
Fuente original: Docker Compose Spec — healthcheck
Este artículo fue publicado originalmente en juanchi.dev
Top comments (0)