Docker Compose starts API before Postgres is ready because depends_on only waits for container start
Docker Compose starts API before Postgres is ready because depends_on only waits for container start
Your docker compose up starts Postgres and the API together. The API opens a connection to db:5432, gets connection refused, exits, and restarts. Locally that is noisy logs and wasted minutes. In CI it becomes a flaky job that passes on rerun, so nobody fixes it. The short depends_on form is usually the cause: it waits for the container to start, not for Postgres to accept connections. To gate startup on readiness, use docker compose depends_on healthcheck condition.
What depends_on actually guarantees
The short syntax:
depends_on:
- db
only controls start order. Compose starts the db container before the api container, but “container started” is not “Postgres ready.” Postgres still has to initialize its data directory, start its listener, and accept TCP connections [link needed]. The API can win that race every time on a cold volume.
The long syntax supports conditions: service_started, service_healthy, and service_completed_successfully [link needed]. service_healthy is the one for a database that must accept connections before dependents start.
Use docker compose depends_on healthcheck condition correctly
1. Add a healthcheck to Postgres. Use pg_isready, which exits non-zero until the server accepts connections [link needed].
services:
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: example
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 3s
retries: 10
start_period: 10s
2. Change the API dependency to the long form and require service_healthy:
api:
build: .
depends_on:
db:
condition: service_healthy
3. Keep a small retry loop in the API anyway. A healthcheck is a startup gate, not a replacement for handling transient disconnects after startup.
4. Validate the rendered config before committing:
docker compose config
You should see the long-form depends_on with condition: service_healthy, not a plain list.
service_healthy requires the dependency to define a healthcheck; without one, Compose has nothing to wait for. The start_period gives Postgres time to initialize before failed checks count against retries [link needed].
Verify the gate actually works
Run the database alone and watch its health state:
docker compose up -d db
docker compose ps
Wait until db reports healthy, then start the API:
docker compose up api
The API should start after the healthcheck passes. Check docker compose logs api for connection errors and docker compose events for start order. The docker compose depends_on healthcheck condition setting is working when the API no longer crash-loops on a clean volume.
Also test the failure path: break the healthcheck command, run docker compose up, and confirm the API waits instead of starting. A gate you have never seen block is not verified.
When NOT to do this
Do not add docker compose depends_on healthcheck condition just because it sounds safer. Skip it when:
- The API already retries database connections indefinitely with backoff, and fast startup matters more than strict ordering.
- The dependency is external and not managed by Compose; Compose cannot healthcheck it.
- You need a one-shot migration to finish, not stay healthy. Use
service_completed_successfullyfor that job. - The healthcheck is expensive or slow and runs on every interval, adding load without catching a real failure mode.
Also avoid treating pg_isready as proof that migrations ran or that the application schema exists. It checks server availability, not application readiness.
Checklist
- Replace short
depends_onwith long-formcondition: service_healthywhere readiness matters. - Add a healthcheck that fails until the dependency is actually usable.
- Set
start_period,interval,timeout, andretriesdeliberately. - Keep application-level retries for post-startup failures.
- Run
docker compose configand test both the passing and failing healthcheck paths.