1. Baseline de readiness final
En este punto el problema ya está identificado: un depends_on corto ordena el inicio de los contenedores pero no espera a que PostgreSQL realmente comience a aceptar conexiones. Ahora reuniremos una pieza limpia, legible y reproducible de compose.yaml que transforma la combinación app + postgres de «a veces arranca» a «arranca de forma predecible». No se trata de complicar por complicar: se trata de expresar una regla de inicio una vez, de modo que la entienda Docker Compose, su futuro equipo y usted mismo dentro de dos semanas.
El baseline de readiness en el contexto actual son dos configuraciones acopladas. La primera parte vive en PostgreSQL y dice: «así es como verifico que realmente estoy lista». La segunda parte vive en la aplicación y dice: «inicio solo después de que la dependencia haya pasado su verificación de salud». Docker Compose describe directamente el problema: al arrancar no espera a que el servicio esté "ready", solo espera a que el contenedor esté "running", y por eso para la base de datos se necesitan healthcheck + depends_on.condition.
Para que sea más concreto, aquí hay un breve esquema de «quién responde de qué»:
| Capa | Dónde vive | Qué garantiza |
|---|---|---|
| Señal de readiness de BD | postgres.healthcheck | «PostgreSQL realmente acepta conexiones» |
| Espera de readiness | app.depends_on.postgres.condition: service_healthy | «La aplicación no arranca hasta que la BD esté healthy» |
| Conexión a la BD | SPRING_DATASOURCE_URL | «La aplicación se conecta a la BD por nombre de servicio y puerto de contenedor» |
2. compose.yaml final por partes
Ahora haremos lo que normalmente se llama "baseline": una plantilla mínima pero profesional. La idea no es escribir el YAML más corto del planeta, sino que se lea como un guion. Un buen compose.yaml se parece a un contrato claro: quién arranca, qué espera y cómo saber que todo funciona realmente.
Empecemos con la parte de la aplicación. Necesitamos dos cosas: un depends_on largo con service_healthy y la configuración de Spring Boot para el modo postgres. Es importante que no cambiamos la imagen ni cambiamos el código de negocio: solo cambia el entorno de ejecución, como corresponde en el mundo de los contenedores.
Fragmento 1 — app: esperamos la BD y nos conectamos
services:
app: # servicio de la aplicación
build: .
depends_on:
postgres:
condition: service_healthy # arrancamos solo cuando postgres esté healthy
environment:
SPRING_PROFILES_ACTIVE: postgres # activamos el perfil con configuraciones para Postgres
# Importante: `postgres` es el nombre del servicio en la red de Compose, y `5432` es el puerto del contenedor
SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/catalog
Aquí el significado clave está en la línea condition: service_healthy: Compose realmente sabe esperar a que la dependencia pase el healthcheck antes de iniciar el servicio dependiente. Y el segundo significado es la URL de conexión: postgres:5432, es decir, nombre del servicio y puerto del contenedor. Si intenta poner localhost, volverá al mundo de «por qué falló». El contenedor considera que localhost es él mismo, no la BD vecina.
En un proyecto real casi con toda seguridad habrá también SPRING_DATASOURCE_USERNAME y SPRING_DATASOURCE_PASSWORD. Deliberadamente no los incluyo en este primer fragmento para que el ojo del principiante no se ahogue en detalles, pero la lógica es simple: esos valores deben coincidir con POSTGRES_USER / POSTGRES_PASSWORD del servicio de base de datos.
Fragmento 2 — postgres: healthcheck con pg_isready
Ahora agregamos lo que hace que "ready" sea medible: healthcheck para PostgreSQL. Docker en la documentación sobre orden de inicio muestra directamente el enfoque: la readiness de la dependencia se expresa mediante healthcheck, y el servicio dependiente espera service_healthy.
services:
postgres: # servicio de base de datos
image: postgres:17 # imagen de Postgres para el entorno actual
environment:
POSTGRES_DB: catalog # nombre de la base de datos que creará el contenedor al primer arranque
POSTGRES_USER: catalog # usuario de la BD
POSTGRES_PASSWORD: catalog # contraseña del usuario de la BD
healthcheck:
# Comando de verificación de readiness: "¿ya se pueden aceptar conexiones?"
test: ["CMD", "pg_isready", "-U", "catalog", "-d", "catalog"]
pg_isready es un pequeño «doctor» que le pregunta a PostgreSQL: «¿ya estás en condiciones de aceptar conexiones o todavía te estás tomando un café?». En la guía de Docker para Java recomiendan exactamente así verificar la BD: healthcheck con pg_isready y esperar service_healthy desde la aplicación.
Ahora agreguemos los parámetros de temporización. No afectan la "corrección" de la idea, sino qué tan adecuadamente funcionará en diferentes máquinas.
services:
postgres:
healthcheck:
interval: 5s # con qué frecuencia verificamos la readiness
timeout: 3s # cuánto esperamos la respuesta de una comprobación
retries: 10 # cuántas comprobaciones fallidas consecutivas se permiten
start_period: 10s # tiempo de "cortesía" para el calentamiento antes de comprobaciones estrictas
Estos cuatro números no son "mágicos", pero deben expresar la idea: "dale a la base un poco de tiempo para arrancar, y luego verifica regularmente hasta que esté bien".
Fragmento 3 — baseline completo
Cuando se junta todo, se obtiene algo así. Sí, ya son más de 10 líneas, pero es un caso donde la compacidad es peor que la legibilidad:
services:
app: # aplicación (Spring Boot)
build: .
depends_on:
postgres:
condition: service_healthy # esperamos hasta que la BD esté healthy
environment:
SPRING_PROFILES_ACTIVE: postgres
# Nos conectamos por el nombre del servicio en la red de Compose, NO por localhost
SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/catalog
# Estas credenciales deben coincidir con POSTGRES_USER/POSTGRES_PASSWORD de postgres
SPRING_DATASOURCE_USERNAME: catalog
SPRING_DATASOURCE_PASSWORD: catalog
ports:
- "8080:8080" # reenvío del puerto de la aplicación al host
postgres: # base de datos
image: postgres:17
environment:
POSTGRES_DB: catalog
POSTGRES_USER: catalog
POSTGRES_PASSWORD: catalog
healthcheck:
# Readiness de la BD: comprobamos si Postgres acepta conexiones
test: ["CMD", "pg_isready", "-U", "catalog", "-d", "catalog"]
interval: 5s
timeout: 3s
retries: 10
start_period: 10s
Si ahora miran el YAML y piensan «¿por qué todo esto no podía ser un solo botón?», entonces felicidades: acaban de sentir por qué los ingenieros DevOps a veces se ven cansados. Pero la buena noticia es que este es un baseline realmente pequeño, comprensible y típico.
Y sí: compose.yaml es el nombre canónico del archivo, se considera la ruta preferida en el modelo de Compose.
3. Inicio real: eventos e indicios
A menudo intentamos juzgar el sistema por un solo indicio: «el contenedor se inició». En el caso de una base de datos esto es especialmente engañoso: el contenedor puede estar Up, el proceso vivo, pero el propio SGBD aún no está listo para las conexiones. Por eso en el modelo de readiness miramos conscientemente la secuencia: “postgres started → postgres healthy → app started”. No es filosofía, es una forma de dejar de depender de la suerte y la velocidad del portátil.
Así es como se puede representar como guion:
sequenceDiagram
participant C as docker compose
participant P as postgres
participant A as "app (Spring Boot)"
C->>P: iniciar contenedor
P-->>P: "inicialización (iniciando)"
C->>P: "healthcheck: pg_isready (bucle)"
P-->>C: "healthy (código de salida 0)"
C->>A: "iniciar contenedor (depends_on: service_healthy)"
A-->>P: conectar jdbc:postgresql://postgres:5432/catalog
A-->>C: iniciado OK
Si el esquema parece demasiado "inteligente", se puede reducir a una simple observación: la aplicación no debe fallar al arrancar con Connection refused solo porque la BD tarda un par de segundos más.
Una buena lógica de inicio se lee casi siempre en los logs como una historia humana. Más o menos así:
postgres-1 | database system is ready to accept connections
app-1 | Started CatalogApplication in 4.123 seconds
Esto no es una "belleza decorativa", sino el resultado principal del día: el stack arranca de forma estable porque la espera está expresada mediante un estado verificable, y no mediante «esperemos 20 segundos y crucemos los dedos».
4. Diagnóstico del baseline de readiness
La fortaleza del baseline de readiness es que no solo "funciona", sino que también es "observable". No arrancamos el stack a ciegas ni adivinamos en posos de café. Miramos los estados y los logs. Y no necesitamos todo un stack de observabilidad, solo dos comandos que ya ha visto: docker compose ps y docker compose logs.
Aquí está el conjunto canónico mínimo. Realmente vale la pena tenerlo en la cabeza como "abrir el capó":
docker compose ps # vemos los estados de los contenedores y healthcheck (healthy/unhealthy)
docker compose logs -f app postgres # leemos los logs de la aplicación y la BD simultáneamente
En docker compose ps no solo le interesa la columna "Up", sino la salud. Cuando un servicio tiene healthcheck, el estado cambia y comienza a tener significado. En la documentación de Docker este significado se describe literalmente: service_healthy significa que la dependencia debe estar "healthy", y "healthy" se define mediante healthcheck.
Un punto psicológico importante: si solo lee los logs de la aplicación, casi con toda seguridad empezará a "curar lo que no es". Por ejemplo, cambiará SPRING_DATASOURCE_URL, aunque el problema no era la URL sino el tiempo de arranque de la BD. Cuando mira los logs de app y postgres simultáneamente, la relación causa-efecto se vuelve aburridamente obvia. Y aburridamente obvio es, de hecho, un cumplido para la infraestructura.
Incluso puede tener una pequeña tabla de interpretación de señales:
| Lo que ve | Qué significa | Qué hacer con el cerebro |
|---|---|---|
| postgres ... Up (healthy) | La BD pasó la verificación | La aplicación tiene derecho a arrancar |
| postgres ... Up sin (healthy) | healthcheck no está definido o no funciona | No se puede usar service_healthy como garantía |
| app ... Exited (1) | La aplicación falló al arrancar | Inmediatamente mire los logs de app y postgres juntos |
| postgres ... (unhealthy) | healthcheck falla | El problema está en los parámetros de la verificación o en la propia BD |
5. Parámetros de healthcheck
Los números en healthcheck no son mágicos. Para un entorno local de app + postgres este conjunto hace exactamente lo que necesitamos: la verificación se realiza con suficiente frecuencia, un intento lento no declara la base como rota, y start_period le da tiempo para superar un arranque en frío.
Si en su máquina PostgreSQL realmente tarda más en despertarse, lo que debe ajustar son estos parámetros, no agregar un fixed-delay en el inicio de la aplicación.
Criterio de calidad del baseline
Una buena señal es que el stack se pueda levantar varias veces seguidas y app ya no falle aleatoriamente con Connection refused al arrancar. Después de eso, el problema se desplaza de la temporización al ciclo de vida de los datos: ya no es importante "acertar la readiness", sino manejar correctamente migraciones, reset y recuperación de la BD local.
6. Errores típicos al configurar el baseline de readiness
Error n.º 1: healthcheck escrito "para salir del paso".
A veces se encuentra test: ["CMD", "true"] o algo similar que siempre tiene éxito. Esto es peor que la ausencia de healthcheck, porque crea una ilusión de fiabilidad: Compose comenzará a considerar el servicio como "healthy" instantáneamente, y la aplicación arrancará y fallará exactamente igual que antes. La readiness debe medir precisamente la disposición de PostgreSQL para aceptar conexiones, y pg_isready es exactamente ese indicador práctico.
Error n.º 2: depends_on se quedó en la forma corta.
La forma corta depends_on: - postgres establece el orden, pero no la readiness. Como resultado, aparentemente "ha establecido la dependencia", pero la condición de carrera permanece. Se necesita la forma larga con condition: service_healthy, porque Compose espera la superación del healthcheck solo para las dependencias marcadas como service_healthy.
Error n.º 3: la aplicación se conecta a localhost o al puerto del host.
Es un hábito clásico después de ejecuciones locales sin Docker: "la BD está en localhost". En la red de Compose, localhost dentro del contenedor es el propio contenedor de la aplicación, no la base. Por lo tanto, la URL JDBC dentro de Compose debe referirse al nombre del servicio postgres y al puerto del contenedor 5432. Si pone el puerto del host, está intentando "sortear" la red de Compose e introduce una fragilidad innecesaria en el sistema.
Error n.º 4: la sangría YAML se comió su condition y no se dio cuenta.
Este es un dolor silencioso: Compose no siempre "grita" como uno quisiera, y una persona con los ojos fácilmente se salta un espacio. Cuando condition no está bajo la clave correcta, Compose simplemente interpreta depends_on como una lista normal y no espera el healthcheck. Si de repente vuelve a aparecer Connection refused al arrancar, lo primero que hay que mirar no es el código Java, sino la estructura del YAML.
Error n.º 5: timeout y start_period demasiado agresivos.
Si se pone start_period: 0s y timeout: 1s, obtendrá "unhealthy" incluso en una base normal, simplemente porque no ha tenido tiempo de arrancar y la verificación es demasiado nerviosa. Docker en el ejemplo de orden de inicio muestra directamente que start_period a menudo se hace notable para que el healthcheck sea honesto con respecto a la fase de arranque de la BD.
GO TO FULL VERSION