Imagina esto: tu configuración de Docker Compose funciona perfectamente en tu máquina local. Subes los cambios a CI y, de repente, todas las pruebas de integración fallan con Connection refused.
El contenedor de la base de datos está "en ejecución". El contenedor de la API está "saludable". El proceso de prueba comienza. Luego no puede conectarse al servicio que necesita.
Este fallo parece aleatorio hasta que recuerdas una cosa: la red Docker local y la red Docker de CI no son el mismo entorno.
La configuración local te engaña
En tu máquina, podrías conectarte a Postgres en localhost:5432.
Dentro de una red de Compose, otro contenedor normalmente debería conectarse a postgres:5432, donde postgres es el nombre del servicio.
En CI, el ejecutor de pruebas puede estar:
- dentro de la red de Compose
- fuera de la red de Compose en el host
- dentro de un contenedor de servicio de CI
- dentro de un ejecutor Docker anidado
Esos cuatro casos usan diferentes nombres de host.
Por eso una cadena de conexión puede ser "correcta" localmente y fallar en el pipeline.
Primero, identifica dónde se ejecuta el proceso de prueba
Antes de cambiar puertos, hazte una pregunta:
¿El comando de prueba se ejecuta dentro de un servicio de Compose o en el host de CI?
Si las pruebas se ejecutan dentro de Compose:
DATABASE_URL=postgres://usuario:contraseña@postgres:5432/appSi las pruebas se ejecutan en el host de CI y Compose publicó el puerto:
DATABASE_URL=postgres://usuario:contraseña@127.0.0.1:5432/appSi las pruebas se ejecutan en un contenedor de CI separado, es posible que ninguna funcione hasta que se configure la red de servicios de la plataforma de CI.
El nombre de host correcto depende de dónde se encuentre el ejecutor de pruebas. Los nombres de servicio funcionan dentro de la red de Compose. Los puertos publicados en localhost funcionan desde el host.
No confíes en depends_on como indicador de disponibilidad
depends_on puede controlar el orden de inicio. No garantiza que Postgres, Redis o tu aplicación estén listos para aceptar conexiones.
La versión incorrecta común:
services:
api:
depends_on:
- postgresEso solo significa que el contenedor postgres se inicia antes que api. No significa que las migraciones se hayan ejecutado. No significa que TCP esté listo. No significa que la base de datos haya aceptado la autenticación.
Usa verificaciones de salud o un script de espera explícito.
services:
postgres:
image: postgres:16
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app"]
interval: 5s
timeout: 5s
retries: 12
api:
depends_on:
postgres:
condition: service_healthyEso aún no resuelve todas las plataformas de CI, pero elimina la condición de carrera más común.
Revisa las cuatro clases de fallo
Cuando veo Connection refused, trabajo en este orden.
La verificación TCP es aburrida pero útil:
node -e "require('net').connect(5432, process.env.DB_HOST).on('connect', () => { console.log('ok'); process.exit(0) }).on('error', e => { console.error(e.message); process.exit(1) })"Si eso falla, tu suite de pruebas de la aplicación no es lo que debes depurar todavía.
Usa diferentes cadenas de conexión para diferentes límites
Un patrón limpio es hacer explícito el límite:
DATABASE_URL_INTERNAL=postgres://app:app@postgres:5432/app
DATABASE_URL_HOST=postgres://app:app@127.0.0.1:5432/appLuego, tu trabajo de CI elige la correcta según dónde se ejecute el comando.
Esto es menos mágico que intentar que una sola URL funcione en todas partes.
Mantén las migraciones separadas de la disponibilidad
Una base de datos puede estar saludable antes de que el esquema esté listo.
Si tu aplicación necesita migraciones, conviértelo en un paso explícito del pipeline:
docker compose up -d postgres
docker compose run --rm migrate
docker compose run --rm testO ejecuta las pruebas dentro de un servicio que espere ambas condiciones:
- salud de la base de datos
- migraciones completadas
- datos de semilla cargados
De lo contrario, obtienes una clase peor de fallo: errores de prueba intermitentes que parecen errores de la aplicación pero que en realidad son condiciones de carrera en la configuración.
La salida de depuración que quiero en cada fallo de CI
No filtres secretos. Sí imprime la forma del entorno.
Salida útil:
- Servicios de Docker Compose y su estado
- registros del contenedor para la dependencia
- host y puerto resueltos, con la contraseña oculta
- nombres de red
- estado de la verificación de salud
- estado de la migración
Ejemplo:
docker compose ps
docker compose logs --tail=80 postgres
docker network lsEl objetivo es hacer que el próximo fallo sea diagnosticable en una sola pasada.
La lección para producción
El dolor de la red en CI es un adelanto del dolor de integración en producción.
Si tus pruebas dependen de la esperanza, probablemente tus despliegues también. Haz explícitos los límites del servicio. Agrega verificaciones de salud. Separa la disponibilidad de las migraciones. Registra los hechos correctos.
Así es como conviertes "funciona en mi máquina" en algo que un pipeline pueda demostrar.
