Por qué `depends_on` falla por defecto
Por defecto, depends_on usa la condición service_started. Esto significa que Docker solo espera a que el contenedor de destino esté lanzado — es decir, a que su proceso principal haya arrancado. Eso no dice nada sobre el estado interno del servicio.
PostgreSQL, como la mayoría de las bases de datos, atraviesa varias fases durante la inicialización: la imagen oficial ejecuta scripts de arranque, crea los roles, inicializa las extensiones y prepara el clúster antes de empezar a aceptar conexiones. Esta secuencia puede tardar desde unos segundos hasta más de treinta segundos en un VPS con un disco cargado, un volumen sin preparar o un conjunto grande de extensiones.
Mientras tanto, su aplicación — que sí respeta la directiva depends_on — ya intenta conectarse, y recibe un rechazo seco.
Las consecuencias prácticas de una race condition en el arranque
connection refused— el socket TCP de PostgreSQL todavía no está abierto y la aplicación falla en la primera llamada PDO o SQLAlchemy.FATAL: role does not exist— PostgreSQL escucha, pero los scripts de inicialización (docker-entrypoint-initdb.d) todavía no han creado el rol ni la base de datos.FATAL: the database system is starting up— el clúster está recuperándose tras una parada limpia; las conexiones se rechazan temporalmente.- Crash loop silencioso — con
restart: unless-stopped, Docker relanza la aplicación indefinidamente, los logs se repiten y el problema se toma por un error de la aplicación. - Falsos positivos en CI — las pruebas de integración fallan de forma intermitente según la velocidad de arranque del runner.
- Dependencias en cascada — una API que depende de una aplicación que depende de la base hereda el mismo problema si la cadena
depends_onno es correcta de forma uniforme.
Requisitos previos: Docker Compose v2 y el plugin oficial
La condición service_healthy no está disponible en Docker Compose v1 (el binario docker-compose en Python, hoy obsoleto). Se admite desde Docker Compose v2, distribuido como plugin en Go bajo el comando docker compose (sin guion).
Para comprobar su versión:
docker compose versionLa salida debe mostrar Docker Compose version v2.x.x o superior. En Debian 12 y Ubuntu 22.04+, el plugin está disponible en los repositorios oficiales de Docker. Si todavía usa docker-compose (v1), migre: el proyecto está archivado y ya no recibe parches de seguridad.
No hace falta ninguna dependencia adicional para PostgreSQL: pg_isready es una herramienta nativa de la imagen oficial postgres, presente en todos los tags desde hace años.
Configurar un healthcheck de PostgreSQL fiable, paso a paso
Entender service_started frente a service_healthy
depends_onacepta tres condiciones:-
service_started(por defecto) — espera a que el contenedor simplemente haya arrancado.
-service_healthy— espera a que el healthcheck del contenedor devuelvahealthy.
-service_completed_successfully— para contenedores de vida corta (jobs, migraciones).Para cualquier base de datos,
service_healthyes la única condición que garantiza que el servicio acepta conexiones.Escribir el healthcheck de PostgreSQL en el servicio `db`
Añada el bloque
healthcheckdirectamente en la definición del serviciodb:services: db: image: postgres:16 environment: POSTGRES_USER: app POSTGRES_PASSWORD: secret POSTGRES_DB: appdb healthcheck: test: ["CMD", "pg_isready", "-U", "app", "-d", "appdb"] interval: 5s timeout: 5s retries: 5 start_period: 30sEl campo
testrecibe una lista: el primer elemento esCMD(Docker ejecuta el comando directamente), seguido de los argumentos.pg_isreadydevuelve0si PostgreSQL está listo para aceptar conexiones con el usuario y la base indicados, y un código distinto de cero en caso contrario — lo que Docker interpreta comohealthyounhealthy.Entender el papel de `start_period`
start_periodes la ventana de gracia concedida al contenedor para inicializarse antes de que los fallos del healthcheck empiecen a contabilizarse enretries. Durante esa ventana, Docker sí ejecuta el healthcheck, pero un fallo no incrementa el contador.Sin
start_period, un PostgreSQL que tarda 15 segundos en inicializarse fallaría sus 5 primeras comprobaciones (interval: 5s× 5 intentos = 25 segundos) y pasaría aunhealthyantes incluso de estar operativo.El valor recomendado son 30 segundos para un PostgreSQL estándar: lo bastante largo para absorber las inicializaciones lentas (primer arranque con volumen vacío, extensiones pesadas) sin retrasar innecesariamente el arranque en régimen permanente.
intervalystart_periodson distintos:intervalmarca el ritmo de las comprobaciones en régimen normal,start_periodprotege la fase de arranque.Escribir el `depends_on` con `condition: service_healthy`
En cada servicio que dependa de la base de datos, sustituya la forma corta de
depends_onpor la forma larga con condición:services: app: image: monapp:latest depends_on: db: condition: service_healthy environment: DATABASE_URL: postgresql://app:secret@db:5432/appdbCon esta configuración, Docker espera a que el healthcheck del servicio
dbdevuelvahealthyantes de arrancarapp. Sidbpasa aunhealthytrasretriesfallos,appno arranca.Probar con `docker compose up`
Lance la stack y observe la secuencia:
docker compose upVerá en los logs líneas de este tipo:
db | database system is ready to accept connections app | Waiting for db to be healthy... app | Starting application serverPara comprobar el estado del healthcheck en cualquier momento:
docker inspect <nom_du_conteneur_db> | grep -A 5 '"Health"'La salida indica
Status: healthy,startingounhealthy, y lista las últimas salidas de la comprobación.Caso Redis: healthcheck adaptado
Redis no dispone de
redis-isready, pero su equivalente esredis-cli ping:redis: image: redis:7-alpine healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 5s timeout: 3s retries: 5 start_period: 10sredis-cli pingdevuelvePONGy el código de salida0si Redis acepta conexiones. Como Redis arranca más rápido que PostgreSQL,start_period: 10ssuele ser suficiente.Caso MySQL / MariaDB: `mysqladmin ping`
Para MySQL o MariaDB, use
mysqladmin ping:mysql: image: mariadb:11 environment: MYSQL_ROOT_PASSWORD: secret MYSQL_DATABASE: appdb healthcheck: test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-psecret"] interval: 5s timeout: 5s retries: 5 start_period: 30sAtención: la contraseña se concatena directamente a
-psin espacio (-psecret), es el comportamiento esperado demysqladmin. Este comando aparece endocker inspect, así que prefiera un secret de Compose o una variable de entorno si la confidencialidad es una restricción.Resolución de problemas: cuatro errores frecuentes
pg_isready: command not found— no está usando la imagen oficialpostgres(ni una imagen derivada que la incluya). Compruébelo condocker compose exec db which pg_isready.Healthcheck en bucle, nunca
healthy— eltestnunca devuelve0. Pruébelo manualmente:docker compose exec db pg_isready -U app -d appdb. Si el comando falla, revise las variables de entornoPOSTGRES_USERyPOSTGRES_DB.start_perioddemasiado corto — en un primer arranque con un volumen vacío, PostgreSQL puede tardar más de 30 segundos. Auméntelo a60su observe los logs:database system was shut down at … LOG: database system is ready to accept connectionsindica el retraso real.Contenedor en
unhealthypermanente — trasretriesfallos, Docker marca el contenedor comounhealthypero no lo reinicia (eso es tarea derestart). Consultedocker inspectpara ver la salida de las últimas comprobaciones e identificar el comando que falla.
Comportamiento por defecto frente a healthcheck
Desplace la tabla
| Caso | Comportamiento por defecto (`service_started`) | Con `service_healthy` |
|---|---|---|
| Primer arranque, volumen vacío | La app arranca antes de que la base esté lista → crash loop | La app espera a que PostgreSQL se inicialice y acepte conexiones |
| Reinicio tras una parada limpia | La app puede arrancar durante la fase de recovery de PostgreSQL | La app queda en espera hasta el final de la recovery |
| Base lenta (extensiones, init pesado) | Race condition según la velocidad del host | Sin race condition: el healthcheck valida el estado real |
| Dependencias en cascada (app → worker → db) | Cada eslabón debe gestionar por su cuenta los reintentos de conexión | La cadena de condiciones garantiza el orden de arranque |
| Pruebas de integración en CI | Resultados intermitentes según la velocidad del runner | Resultados deterministas |
| Redis o MySQL en lugar de PostgreSQL | El mismo problema: `depends_on` por defecto no distingue | La misma solución, con el comando de comprobación adaptado a cada motor |
`pg_isready` o `SELECT 1`: ¿cuál elegir?
En la práctica se ven a menudo dos variantes de healthcheck de PostgreSQL:
- ["CMD", "pg_isready", "-U", "postgres"]
- ["CMD-SHELL", "psql -U postgres -c 'SELECT 1'"]"
pg_isready es más fiable por una razón sencilla: solo comprueba la capacidad del servidor para aceptar conexiones TCP, sin abrir una sesión SQL. Devuelve 0 en cuanto el servidor escucha y acepta el saludo de conexión, que es exactamente lo que una aplicación necesita para intentar su propia conexión.
SELECT 1 mediante psql abre una sesión SQL real y ejecuta una consulta. Es una prueba más profunda, pero puede fallar por motivos ajenos a la disponibilidad del servidor (cuota de conexiones alcanzada, pg_hba.conf mal configurado). Para un healthcheck, la prueba mínima y directa es preferible.
Adaptar el healthcheck a Redis y MySQL
Para Redis, sustituya pg_isready por CMD redis-cli PING — el comando devuelve PONG en cuanto el servidor acepta conexiones. Para MySQL o MariaDB, use CMD mysqladmin ping -h localhost -u root --password=$$MYSQL_ROOT_PASSWORD: ajuste start_period: 60s, porque la inicialización de una base MySQL tarda más que la de PostgreSQL. El patrón condition: service_healthy es idéntico sea cual sea el servicio de destino.
Enlaces internos y próximos pasos
El healthcheck service_healthy es uno de los ajustes de robustez que conviene activar en producción. Otros puntos merecen la misma atención antes de un despliegue duradero: la política de reinicio restart: unless-stopped, los límites de recursos deploy.resources.limits y la rotación de logs logging.options. Encontrará la lista de comprobación completa en Docker Compose en producción: 10 puntos que verificar.
Si su stack crece — varios servicios, varios hosts — un reverse proxy como Caddy o Traefik se vuelve imprescindible para gestionar el enrutamiento HTTPS. La guía Caddy, Traefik o Nginx Proxy Manager detalla los criterios de elección según su perfil.
Para automatizar el despliegue de toda la infraestructura (VPS, Docker, configuración) de forma reproducible, Ansible para automatizar sus servidores VPS le guiará paso a paso.