Cambiar tema

Dependencias entre servicios en Docker Compose: healthcheck para resolver el orden de arranque de la base de datos

Easton editorial illustration: environment switchboard

Son las diez de la noche del viernes y el mismo error lleva repitiéndose en la terminal más de una docena de veces.

El contenedor de la aplicación se reinicia sin parar; la base de datos sí arranca, pero siempre un paso tarde. Revisas docker-compose.yml: depends_on está configurado. ¿Por qué sigue fallando?

Este problema ha atormentado a muchos desarrolladores. En local, con docker-compose up, las dos primeras veces casi siempre fallan; hay que esperar unos segundos y reiniciar varias veces hasta que todo arranca bien.

La causa es simple: depends_on de Docker solo controla el orden de arranque de contenedores, no si el servicio está realmente listo.

En este artículo te enseño paso a paso:

  • Las tres condition de depends_on (el 90 % solo conoce la opción por defecto)
  • El healthcheck correcto para PostgreSQL y MySQL (con configuración completa)
  • La alternativa moderna a los scripts wait-for-it
  • Una lista de diagnóstico para que tus contenedores arranquen de forma estable

Por qué depends_on no basta: arranque ≠ listo

En la documentación oficial de Docker hay una frase clave que muchos pasan por alto:

Compose does not wait until a container is “ready”, only until it’s running.

Traducción: Compose solo espera a que el contenedor arranque, no a que el servicio sea usable.

Diferencia entre arranque del contenedor y servicio listo

Imagina el arranque de un contenedor PostgreSQL:

  1. 0 s: Docker levanta el contenedor, arranca el proceso postgres ← depends_on da luz verde aquí
  2. 2 s: inicializa el directorio de datos
  3. 5 s: carga la configuración
  4. 8 s: ejecuta scripts init (si los hay)
  5. 12 s: por fin ready, acepta conexiones

Entre medias hay un gap de unos 12 segundos. Si tu app web intenta conectar en el segundo 1, el resultado será “Connection refused”.

En casos reales puede ser peor. Mantuve un proyecto legacy cuyo script de inicialización importaba 500 MB de datos de prueba; solo ese paso tardaba 40 segundos. Con depends_on por defecto, el contenedor de la app se caía y reiniciaba al menos cinco veces antes de conectar.

Las tres condition de depends_on

Mucha gente no sabe que depends_on admite tres condition:

services:
  web:
    depends_on:
      db:
        condition: service_started  # Por defecto: basta con que el contenedor esté running
        # condition: service_healthy  # Espera a que pase el healthcheck
        # condition: service_completed_successfully  # Espera salida exitosa (ideal para init containers)

service_started (por defecto): continúa en cuanto el contenedor está en estado running. Por eso depends_on a veces no basta.

service_healthy: espera a que el healthcheck pase y el contenedor pase a “healthy”. Es lo que realmente necesitamos.

service_completed_successfully: espera a que el contenedor termine con código de salida 0. Ideal para migraciones u otras tareas puntuales.

¿Por qué service_healthy no es el valor por defecto?

Puede que te preguntes: si service_healthy funciona tan bien, ¿por qué no es el default?

Dos razones:

  1. No todos los servicios necesitan healthcheck (por ejemplo, workers stateless)
  2. El healthcheck lo configuras tú; Docker no sabe cuándo tu servicio está “listo”

Eso nos lleva al siguiente tema: cómo configurar el healthcheck.

Guía completa de healthcheck

El principio es directo: Docker ejecuta periódicamente un comando; si devuelve 0, el servicio está healthy; si devuelve 1, unhealthy.

Configuración completa de healthcheck

services:
  db:
    image: postgres:16
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]  # Comando de comprobación
      interval: 10s       # Comprobar cada 10 segundos
      timeout: 5s         # Timeout por comprobación: 5 s
      retries: 3          # Marcar unhealthy tras 3 fallos consecutivos
      start_period: 30s   # Los fallos en los primeros 30 s no cuentan para retries

Los cinco parámetros importan. Vamos uno a uno.

test: comando de comprobación

Hay dos formatos:

# Forma 1: usar shell (recomendado)
test: ["CMD-SHELL", "pg_isready -U postgres"]

# Forma 2: ejecutar comando directo (sin shell)
test: ["CMD", "pg_isready", "-U", "postgres"]

En la mayoría de casos basta CMD-SHELL, porque permite pipes, redirecciones y otras características de shell.

Trampa habitual: la herramienta del comando debe existir en la imagen. Si usas curl para comprobar HTTP pero la imagen no lo incluye, el healthcheck fallará siempre. Me pasó una vez; tardé media hora en darme cuenta de que había que añadir RUN apk add curl al Dockerfile.

interval: intervalo de comprobación

Con qué frecuencia comprobar. Muy frecuente gasta recursos; muy lento, reacciona tarde.

  • 10 s es un buen valor por defecto para la mayoría de escenarios
  • Servicios críticos como bases de datos: 5 s
  • Servicios ligeros: 15–30 s también vale

timeout: timeout por comprobación

Tiempo máximo de una comprobación. Si el comando se cuelga, Docker espera hasta timeout y abandona.

Muy corto provoca falsos positivos; muy largo retrasa la detección de fallos. 5–10 s suele ser seguro.

retries: reintentos tras fallo

Cuántos fallos consecutivos hacen falta para marcar unhealthy.

Es un mecanismo anti-ruido: un jitter de red o una carga puntual pueden fallar una comprobación; retries hace el sistema más robusto.

3–5 es razonable. retries=1 es demasiado sensible; retries=10, demasiado lento.

start_period: periodo de gracia al arranque (el más ignorado)

Es el parámetro que más se pasa por alto y el que más dolores de cabeza causa.

Los fallos dentro de start_period no cuentan para retries. En otras palabras, das al servicio un “colchón” de arranque.

¿Por qué importa? Las bases de datos tardan en arrancar. PostgreSQL inicializa el directorio de datos; MySQL carga índices. Sin start_period, el healthcheck empieza a fallar en el segundo 2 y el servicio puede quedar unhealthy antes de agotar retries.

Valores recomendados:

  • PostgreSQL/MySQL: 30–60 s
  • Servicios ligeros (Redis): 15–30 s
  • Con scripts de inicialización pesados: hasta 120 s

Yo suelo poner 60 s: prefiero esperar un poco más a tener falsos unhealthy.

Errores frecuentes y cómo evitarlos

Error 1: referencia incorrecta a variables de entorno

# ❌ Incorrecto: Compose interpola antes del arranque y toma variables del host
test: ["CMD", "mysqladmin", "ping", "-p$MYSQL_ROOT_PASSWORD"]

# ✅ Correcto: escapa con $$ para que el shell del contenedor lo resuelva
test: ["CMD-SHELL", "mysqladmin ping -p$$MYSQL_ROOT_PASSWORD"]

Error 2: start_period demasiado corto

# ❌ La base de datos aún no terminó de inicializar y ya empieza a contar fallos
healthcheck:
  test: ["CMD", "pg_isready"]
  interval: 5s
  retries: 3
  start_period: 10s  # ¡Demasiado corto!

# ✅ Da tiempo suficiente de arranque
healthcheck:
  start_period: 60s  # Mucho más tranquilo

Error 3: la herramienta de comprobación no existe

Este error es especialmente traicionero porque Docker falla en silencio.

# ❌ Si la imagen no tiene curl, el healthcheck fallará siempre
test: ["CMD", "curl", "-f", "http://localhost/health"]

# ✅ Asegura que la herramienta exista, o usa la que trae la imagen
test: ["CMD", "wget", "--spider", "http://localhost/health"]  # Alpine incluye wget

Con el healthcheck listo, veamos bases de datos concretas.

Healthcheck de PostgreSQL en la práctica

La imagen oficial de PostgreSQL trae una herramienta muy útil: pg_isready.

Está pensada para comprobar si PostgreSQL está ready; es más fiable que escribir una consulta SQL a mano.

Configuración básica (recomendada)

version: '3.8'

services:
  web:
    image: node:20-alpine
    depends_on:
      db:
        condition: service_healthy  # Clave: esperar al healthcheck
        restart: true  # Si la base de datos reinicia, la app también
    environment:
      DATABASE_URL: postgresql://postgres:password@db:5432/myapp
    command: npm start

  db:
    image: postgres:16
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: password
      POSTGRES_DB: myapp
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres -d myapp"]
      interval: 10s
      timeout: 5s
      retries: 3
      start_period: 60s
    volumes:
      - postgres_data:/var/lib/postgresql/data

volumes:
  postgres_data:

Detalle del comando pg_isready

pg_isready -U postgres -d myapp
  • -U: nombre de usuario; debe existir
  • -d: nombre de base de datos (opcional, pero recomendable)

¿Por qué -U? Sin él, pg_isready intenta el usuario del sistema y los logs se llenan de warnings. No rompe nada, pero molesta.

Configuración avanzada: añadir una consulta real

pg_isready solo comprueba si el puerto responde, no si la base puede ejecutar consultas. Si necesitas algo más estricto:

healthcheck:
  test: ["CMD-SHELL", "pg_isready -U postgres && psql -U postgres -d myapp -c 'SELECT 1'"]
  interval: 10s
  timeout: 10s  # Aumenta timeout por la consulta extra
  retries: 3
  start_period: 60s

SELECT 1 es la consulta más simple; si funciona, la base no solo arrancó, sino que procesa SQL.

En la mayoría de casos basta pg_isready básico.

Efecto en el arranque real

Tras configurarlo, arranca y observa:

$ docker-compose up

Creating network "myapp_default" ... done
Creating myapp_db_1 ... done
Waiting for myapp_db_1 to be healthy... Fíjate en esta línea
Creating myapp_web_1 ... done

db_1   | PostgreSQL init process complete; ready for start up.
db_1   | database system is ready to accept connections
web_1  | Server listening on port 3000 La app arranca cuando la base está ready

Verás una pausa clara: Docker espera a que db pase a healthy. Puede tardar 30–60 s, pero a cambio obtienes arranques sin fallos.

Diagnóstico: el contenedor sigue unhealthy

Si la base de datos permanece unhealthy, revisa los logs del healthcheck:

# Ver estado de salud del contenedor
$ docker inspect --format='{{json .State.Health}}' myapp_db_1 | jq

{
  "Status": "unhealthy",
  "FailingStreak": 5,
  "Log": [
    {
      "Start": "2024-12-17T03:15:30Z",
      "End": "2024-12-17T03:15:30Z",
      "ExitCode": 1,
      "Output": "pg_isready: could not connect to server: Connection refused"
    }
  ]
}

Causas habituales:

  1. start_period demasiado corto: la base aún inicializa cuando empiezan a contarse fallos
  2. Usuario o base de datos incorrectos: pg_isready no puede conectar
  3. PostgreSQL no arrancó bien: revisa docker logs myapp_db_1

Healthcheck de MySQL en la práctica

En MySQL el healthcheck suele usar mysqladmin ping, herramienta de administración incluida.

Configuración básica (recomendada)

version: '3.8'

services:
  web:
    image: node:20-alpine
    depends_on:
      db:
        condition: service_healthy
        restart: true
    environment:
      DATABASE_URL: mysql://root:password@db:3306/myapp
    command: npm start

  db:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD: password
      MYSQL_DATABASE: myapp
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-ppassword"]
      interval: 10s
      timeout: 5s
      retries: 3
      start_period: 60s
    volumes:
      - mysql_data:/var/lib/mysql

volumes:
  mysql_data:

Detalle del comando mysqladmin ping

mysqladmin ping -h localhost -u root -ppassword
  • -h: host (dentro del contenedor, localhost)
  • -u: usuario
  • -p: contraseña (sin espacio entre -p y la contraseña)

Si MySQL está bien, devuelve:

mysqld is alive

Código de salida 0: healthcheck superado.

Forma correcta de manejar la contraseña

Opción 1: contraseña directa (entorno de desarrollo)

healthcheck:
  test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-ppassword"]

Directo, pero la contraseña queda en el archivo.

Opción 2: variable de entorno (recomendado)

db:
  environment:
    MYSQL_ROOT_PASSWORD: password
  healthcheck:
    test: ["CMD-SHELL", "mysqladmin ping -h localhost -u root -p$$MYSQL_ROOT_PASSWORD"]
    # Nota: usa $$ en lugar de $

Aquí hay una trampa: debes usar $$, no $.

¿Por qué? Docker Compose interpola variables antes del arranque. Con $MYSQL_ROOT_PASSWORD, Compose busca la variable en el host, no en el contenedor. $$ le dice a Compose: “no lo toques; que el shell del contenedor lo resuelva”.

Opción 3: comprobación sin contraseña (la más simple, pero discutible)

healthcheck:
  test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]

Algunas configuraciones de MySQL permiten conexión local sin contraseña. En producción no lo recomiendo.

Consideraciones especiales de MySQL 8.0

MySQL 8.0 usa por defecto el plugin caching_sha2_password, lo que puede impedir conexiones de clientes antiguos. Si tu app falla por autenticación, puedes forzar el plugin clásico:

db:
  image: mysql:8.0
  command: --default-authentication-plugin=mysql_native_password
  environment:
    MYSQL_ROOT_PASSWORD: password
    MYSQL_DATABASE: myapp
  healthcheck:
    test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-ppassword"]
    interval: 10s
    timeout: 5s
    retries: 3
    start_period: 60s

Problemas frecuentes

Problema 1: Access denied for user ‘root’@‘localhost’

Contraseña incorrecta o variable de entorno que no se aplicó. Comprueba:

  1. Ortografía de MYSQL_ROOT_PASSWORD
  2. Que la contraseña del healthcheck coincida
  3. Que hayas usado $$ para escapar

Problema 2: el contenedor tarda mucho y permanece en starting

MySQL tarda en inicializar el directorio de datos, sobre todo en el primer arranque. Asegura un start_period amplio; 60 s suele bastar. Con scripts init que importan muchos datos, puede hacer falta 120 s o más.

Problema 3: el healthcheck pasa, pero la app no conecta

Puede ser red o configuración de la app. Comprueba:

  1. Cadena de conexión correcta (host db, no localhost)
  2. Configuración de red Docker
  3. docker network inspect para ver si los contenedores comparten red

Healthcheck de otras bases de datos y servicios

Con PostgreSQL y MySQL dominados, el resto es similar. Aquí va una referencia rápida.

Redis

redis:
  image: redis:7-alpine
  healthcheck:
    test: ["CMD", "redis-cli", "ping"]
    interval: 10s
    timeout: 3s
    retries: 3
    start_period: 15s

redis-cli ping devuelve PONG con código 0. Redis arranca rápido; start_period puede ser corto.

MongoDB

mongo:
  image: mongo:7
  healthcheck:
    test: ["CMD", "mongosh", "--eval", "db.adminCommand('ping')"]
    interval: 10s
    timeout: 5s
    retries: 3
    start_period: 30s

Nota: desde MongoDB 6.0 se usa mongosh en lugar del antiguo mongo. En versiones anteriores:

test: ["CMD", "mongo", "--eval", "db.adminCommand('ping')"]

RabbitMQ

rabbitmq:
  image: rabbitmq:3-management-alpine
  healthcheck:
    test: ["CMD", "rabbitmq-diagnostics", "ping"]
    interval: 10s
    timeout: 5s
    retries: 3
    start_period: 40s

RabbitMQ arranca lento; start_period de 40 s o más.

Servicio HTTP genérico

Si el servicio expone /health o /ping, puedes usar wget o curl:

api:
  image: myapp:latest
  healthcheck:
    test: ["CMD", "wget", "--spider", "--quiet", "http://localhost:8080/health"]
    # O con curl (si la imagen lo incluye)
    # test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
    interval: 10s
    timeout: 3s
    retries: 3
    start_period: 20s

--spider hace que wget solo compruebe sin descargar; --quiet suprime logs.

Nota: asegura que la imagen incluya wget o curl. Alpine trae wget; Debian/Ubuntu suelen traer curl.

Servicios sin herramienta dedicada

Si no hay utilidad de healthcheck, netcat (nc) puede comprobar el puerto:

service:
  image: some-service:latest
  healthcheck:
    test: ["CMD-SHELL", "nc -z localhost 9000 || exit 1"]
    interval: 10s
    timeout: 3s
    retries: 3
    start_period: 30s

nc -z solo comprueba si el puerto está abierto, sin conexión real. Es más tosco y no garantiza que el servicio esté ready.

wait-for-it: ¿sigue haciendo falta?

Si buscas orden de arranque en Docker, verás muchos artículos recomendando wait-for-it o scripts similares.

La idea: añadir lógica de espera en el entrypoint de la app y comprobar si el puerto TCP del servicio dependiente responde.

Enfoque clásico con wait-for-it

web:
  image: node:20-alpine
  depends_on:
    - db  # depends_on simple, sin comprobar salud
  volumes:
    - ./wait-for-it.sh:/wait-for-it.sh  # Montar script
  command: ["/wait-for-it.sh", "db:5432", "--", "npm", "start"]

wait-for-it.sh comprueba db:5432 en bucle hasta que responde y entonces ejecuta npm start.

¿Por qué ya no se recomienda?

La mejor práctica en 2024 es: si puedes usar healthcheck nativo, no uses scripts.

Razones:

  1. Configuración más clara: la lógica de salud vive en el servicio de base de datos; las dependencias se ven de un vistazo
  2. Sin archivos extra: no mantienes scripts ni montas volumes
  3. Más potente: healthcheck comprueba disponibilidad real; TCP solo dice que el puerto responde
  4. Mejor reutilización: configuras healthcheck una vez y todos los servicios dependientes se benefician

Hay un artículo muy citado: “Forget wait-for-it, use docker-compose healthcheck and depends_on instead”.

¿Cuándo wait-for-it sigue teniendo sentido?

Dos casos especiales:

Caso 1: no puedes modificar la imagen ni el compose

Por ejemplo, imagen de terceros sin healthcheck y sin permiso para añadirlo. Entonces wait-for-it en el lado de la app puede ser la única opción.

Caso 2: esperar varios servicios

./wait-for-it.sh db:5432 redis:6379 rabbitmq:5672 -- npm start

depends_on también admite varios servicios, pero wait-for-it puede ser más conciso en la línea de comando. No es un escenario muy frecuente.

Otras alternativas

Además de wait-for-it:

  • dockerize: escrito en Go, más funciones, soporta plantillas de variables de entorno
  • wait-for: versión simplificada en shell puro
  • docker-compose-wait: en Python, soporta comprobaciones HTTP

Pero en 2024, si puedes usar healthcheck, evita complicarte.

Lista de diagnóstico y buenas prácticas

¿Configurado y sigue fallando? Sigue esta lista; el 90 % de los casos se resuelven.

Comandos de diagnóstico rápido

# 1. Ver estado de todos los contenedores
$ docker-compose ps

NAME       COMMAND    SERVICE   STATUS              PORTS
myapp_db   postgres   db        healthy             5432/tcp
myapp_web  npm start  web       running             0.0.0.0:3000->3000/tcp

# 2. Ver detalle del healthcheck
$ docker inspect --format='{{json .State.Health}}' myapp_db_1 | jq

# 3. Ver logs de contenedores
$ docker-compose logs db
$ docker-compose logs web

# 4. Seguir logs en tiempo real
$ docker-compose logs -f --tail=100

Árbol de decisión para problemas frecuentes

Problema: el contenedor permanece en starting y nunca pasa a healthy

  1. ¿start_period demasiado corto? → prueba 60 s
  2. ¿Comando de healthcheck correcto? → docker inspect para ver el comando real
  3. Ejecuta el comando manualmente → docker exec -it myapp_db_1 pg_isready -U postgres

Problema: pasa a unhealthy y vuelve a healthy, en bucle

  1. interval demasiado corto o recursos justos → prueba 10 s o 15 s
  2. retries demasiado bajo → prueba 5
  3. La base de datos tiene problemas de rendimiento → revisa sus logs

Problema: healthcheck OK, pero la app no conecta

  1. ¿Cadena de conexión correcta? → host = nombre del servicio (db), no localhost
  2. ¿Puertos correctos? → entre contenedores usa el puerto interno (5432), no el mapeado al host
  3. ¿Misma red? → confirma que todos los servicios comparten network

Buenas prácticas en producción

1. Valores recomendados (configuración conservadora)

healthcheck:
  interval: 10s          # Equilibrio entre respuesta y consumo
  timeout: 5s            # Tiempo suficiente para el comando
  retries: 5             # Tolera fallos puntuales
  start_period: 60s      # Tiempo amplio para arranque de base de datos

Estos valores son estables en la mayoría de escenarios. Con scripts init pesados, start_period puede subir a 120 s.

2. Usar estrategia restart

web:
  depends_on:
    db:
      condition: service_healthy
      restart: true  # Si la base reinicia, la app también
  restart: unless-stopped  # Reinicio automático al salir

restart: true garantiza que, si la base se actualiza o reinicia, los servicios dependientes reinician y reconectan.

3. Límites de recursos

El healthcheck consume recursos, aunque pocos. Si el sistema va justo:

healthcheck:
  interval: 30s  # Alarga el intervalo
  timeout: 3s    # Acorta timeout

En la práctica, el overhead del healthcheck suele ser despreciable salvo con cientos de contenedores.

4. Monitorizar el estado de salud

En producción conviene seguir el healthcheck con herramientas de monitorización. Los eventos de Docker pueden ir a Prometheus, Grafana, etc.

# docker-compose.yml
services:
  db:
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 10s
      timeout: 5s
      retries: 3
      start_period: 60s
    labels:
      - "prometheus.io/scrape=true"  # Para que Prometheus recoja el estado

5. Configuración multi-entorno

Desarrollo y producción pueden diferir:

# docker-compose.yml (desarrollo)
db:
  healthcheck:
    start_period: 30s  # Menos datos, arranque más rápido

# docker-compose.prod.yml (producción)
db:
  healthcheck:
    start_period: 120s  # Más datos, arranque más lento
    interval: 5s        # Comprobaciones más frecuentes

Al arrancar, indica los archivos:

docker-compose -f docker-compose.yml -f docker-compose.prod.yml up

Trucos de depuración

Truco 1: probar el comando de healthcheck a mano

Entra al contenedor y ejecuta el comando para ver qué falla:

$ docker exec -it myapp_db_1 sh
/# pg_isready -U postgres -d myapp
/var/run/postgresql:5432 - accepting connections
/# echo $?
0 Código 0 = éxito

Truco 2: desactivar temporalmente el healthcheck

Al depurar, comenta el healthcheck y usa depends_on simple para descartar que el problema sea el propio healthcheck:

web:
  depends_on:
    - db  # Modo simple temporal
    # db:
    #   condition: service_healthy

Cuando la app conecte bien, vuelve a activar el healthcheck.

Truco 3: ver eventos de Docker

Docker registra todos los eventos de contenedor, incluidos cambios de healthcheck:

$ docker events --filter 'event=health_status'

2024-12-17T03:15:30.123456789Z container health_status: healthy (name=myapp_db_1)
2024-12-17T03:16:45.987654321Z container health_status: unhealthy (name=myapp_db_1)

Te ayuda a ver cuándo pasó a unhealthy y cruzarlo con los logs.

Conclusión

Volvamos al problema del inicio: ¿por qué depends_on no basta?

La respuesta en tres palabras: no es suficiente.

depends_on por defecto solo ordena el arranque de contenedores, no la disponibilidad del servicio. Que el contenedor de la base de datos esté running no significa que acepte conexiones; ese gap es la raíz de la mayoría de incidentes.

La solución es directa:

  1. Añade healthcheck a la base de datos con pg_isready o mysqladmin ping
  2. Usa condition: service_healthy para que la app espere al healthcheck
  3. Configura un start_period razonable (desde 60 s) para dar tiempo de inicialización

Con estos tres pasos, los fallos de arranque suelen desaparecer.

Acciones rápidas:

  • Ahora mismo: copia la configuración de PostgreSQL o MySQL de este artículo a tu proyecto y ajusta variables de entorno
  • Esta noche: actualiza los depends_on del equipo a service_healthy
  • Próxima reunión: comparte con el equipo para unificar el estándar

Si te encuentras un caso que no cubrimos, déjalo en los comentarios. Docker Compose tiene más trampas; podemos ir rellenándolas juntos.

Por último, si este artículo te ayudó con un problema de arranque que llevabas tiempo arrastrando, un like me sirve de feedback. Esos momentos de “por fin funciona” son justo lo que me motiva a escribir.

Flujo completo de configuración de healthcheck en Docker Compose

Soluciona fallos de arranque cuando la base de datos aún no está lista, con plantillas completas para PostgreSQL y MySQL

⏱️ Estimated time: 30 min

  1. 1

    Step 1: Entender la causa: las limitaciones de depends_on

    Causa raíz:
    • depends_on de Docker solo controla el orden de arranque de contenedores, no si el servicio está realmente listo
    • Compose solo espera a que el contenedor esté en ejecución, no a que el servicio sea usable
    • Documentación oficial de Docker: Compose does not wait until a container is "ready", only until it's running
    • Traducción: Compose solo espera a que el contenedor arranque, no a que el servicio sea usable

    Diferencia entre arranque del contenedor y servicio listo:
    • 0 s: Docker levanta el contenedor, arranca el proceso postgres (depends_on da luz verde aquí)
    • 2 s: inicializa el directorio de datos
    • 5 s: carga la configuración
    • 8 s: ejecuta scripts init (si los hay)
    • 12 s: por fin ready, acepta conexiones

    Entre medias hay un gap de unos 12 segundos; si tu app web intenta conectar en el segundo 1, el resultado será Connection refused.
  2. 2

    Step 2: Las tres condition de depends_on

    Tres condition de depends_on:

    1. service_started (por defecto)
    • Solo espera el arranque del contenedor, no la disponibilidad del servicio
    • No recomendado

    2. service_healthy (recomendado)
    • Espera arranque del contenedor y healthcheck superado
    • Garantiza que el servicio esté realmente listo

    3. service_completed_successfully
    • Espera a que el contenedor termine con éxito
    • Ideal para tareas puntuales

    Recomendación: service_healthy
    • En docker-compose.yml: depends_on: db: condition: service_healthy
    • Garantiza que la base de datos esté lista antes de arrancar la aplicación
  3. 3

    Step 3: healthcheck correcto para PostgreSQL y MySQL

    Healthcheck de PostgreSQL:
    • Usa pg_isready para comprobar si la base de datos está lista
    • Ejemplo:
    healthcheck:
    test: ["CMD-SHELL", "pg_isready -U postgres"]
    interval: 5s
    timeout: 5s
    retries: 5
    start_period: 60s (tiempo suficiente para inicializar)

    Healthcheck de MySQL:
    • Usa mysqladmin ping para comprobar si la base de datos está lista
    • Ejemplo:
    healthcheck:
    test: ["CMD-SHELL", "mysqladmin ping -h localhost -u root -p$MYSQL_ROOT_PASSWORD"]
    interval: 5s
    timeout: 5s
    retries: 5
    start_period: 60s

    Configuración completa:
    • Define healthcheck en docker-compose.yml (comando, intervalo, timeout, reintentos)
    • Usa condition: service_healthy en depends_on
    • Garantiza que la base de datos esté lista antes de arrancar la aplicación
  4. 4

    Step 4: Solución de problemas y buenas prácticas

    Lista de diagnóstico:
    1. Comprobar que el comando de healthcheck es correcto
    • Ejecútalo manualmente dentro del contenedor y confirma que devuelve 0
    • Ejemplo: docker exec -it db_container pg_isready -U postgres

    2. Desactivar temporalmente el healthcheck
    • Al depurar, comenta el healthcheck
    • Usa depends_on simple para descartar problemas del propio healthcheck

    3. Revisar eventos de Docker
    • Usa docker events para ver arranque y cambios de healthcheck

    Buenas prácticas:
    1. Añade healthcheck a la base de datos con pg_isready o mysqladmin ping
    2. Usa condition: service_healthy para que la app espere al healthcheck
    3. Configura un start_period razonable (desde 60 s) para dar tiempo de inicialización

    Con estos tres pasos, los fallos de arranque de contenedores suelen desaparecer.

    Alternativa moderna a wait-for-it:
    • Combina depends_on + healthcheck
    • Sin scripts extra, configuración simple y fiable
    • Válido para cualquier proyecto Docker Compose

FAQ

¿Por qué depends_on no basta? ¿Qué diferencia hay entre arranque del contenedor y servicio listo?
Causa raíz: depends_on de Docker solo controla el orden de arranque de contenedores, no si el servicio está realmente listo; Compose solo espera a que el contenedor esté en ejecución, no a que el servicio sea usable.

En la documentación oficial hay una frase clave que muchos pasan por alto: Compose does not wait until a container is "ready", only until it's running. Traducción: Compose solo espera a que el contenedor arranque, no a que el servicio sea usable.

Diferencia temporal: imagina el arranque de un contenedor PostgreSQL:
• 0 s: Docker levanta el contenedor, arranca postgres (depends_on da luz verde aquí)
• 2 s: inicializa el directorio de datos
• 5 s: carga la configuración
• 8 s: ejecuta scripts init (si los hay)
• 12 s: por fin ready, acepta conexiones

Entre medias hay un gap de unos 12 segundos; si tu app web intenta conectar en el segundo 1, el resultado será Connection refused.
¿Cuáles son las tres condition de depends_on?
Tres condition de depends_on:

1) service_started (por defecto):
• Solo espera el arranque del contenedor, no la disponibilidad del servicio
• No recomendado

2) service_healthy (recomendado):
• Espera arranque del contenedor y healthcheck superado
• Garantiza que el servicio esté realmente listo
• En docker-compose.yml: depends_on: db: condition: service_healthy

3) service_completed_successfully:
• Espera a que el contenedor termine con éxito
• Ideal para tareas puntuales

Recomendación: service_healthy para que la app arranque solo cuando la base de datos esté lista.
¿Cómo configurar healthcheck para PostgreSQL y MySQL?
Healthcheck de PostgreSQL:
• Usa pg_isready para comprobar si la base de datos está lista
• Ejemplo:
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 5
start_period: 60s (tiempo suficiente para inicializar)

Healthcheck de MySQL:
• Usa mysqladmin ping para comprobar si la base de datos está lista
• Ejemplo:
healthcheck:
test: ["CMD-SHELL", "mysqladmin ping -h localhost -u root -p$MYSQL_ROOT_PASSWORD"]
interval: 5s
timeout: 5s
retries: 5
start_period: 60s

Configuración completa: define healthcheck en docker-compose.yml (comando, intervalo, timeout, reintentos) y usa condition: service_healthy en depends_on para que la base de datos esté lista antes de arrancar la aplicación.
¿Cómo diagnosticar problemas de healthcheck?
Lista de diagnóstico:

1) Comprobar que el comando de healthcheck es correcto:
• Ejecútalo manualmente dentro del contenedor y confirma que devuelve 0
• Ejemplo: docker exec -it db_container pg_isready -U postgres
• Código de salida 0 indica éxito

2) Desactivar temporalmente el healthcheck:
• Al depurar, comenta el healthcheck
• Usa depends_on simple para descartar problemas del propio healthcheck
• Cuando la app conecte bien, vuelve a añadir el healthcheck

3) Revisar eventos de Docker:
• Usa docker events para ver arranque y cambios de healthcheck
¿Cuáles son las buenas prácticas de healthcheck?
Buenas prácticas:
1) Añade healthcheck a la base de datos con pg_isready o mysqladmin ping
2) Usa condition: service_healthy para que la app espere al healthcheck
3) Configura un start_period razonable (desde 60 s) para dar tiempo de inicialización

Con estos tres pasos, los fallos de arranque de contenedores suelen desaparecer.

Alternativa moderna a wait-for-it:
• Combina depends_on + healthcheck
• Sin scripts extra, configuración simple y fiable
• Válido para cualquier proyecto Docker Compose

Acciones rápidas:
• Ahora mismo: copia la configuración de PostgreSQL o MySQL de este artículo a tu proyecto y ajusta variables de entorno
• Esta noche: actualiza los depends_on del equipo a service_healthy
• Próxima reunión: comparte con el equipo para unificar el estándar

16 min de lectura · Publicado el: 17 dic 2025 · Actualizado el: 21 ago 2026

Comentarios

Inicia sesión con GitHub para dejar un comentario

Easton BlogEaston Blog