Docker Compose — depends_on does not wait for readiness
Fix startup races where an app starts before its database or queue is ready by adding healthchecks and condition-based dependencies, plus application retries.
Symptoms
- The app container exits with connection refused right after docker compose up
- The database container is running but not yet accepting connections
- Restarting the app manually makes it work
- depends_on is set, but the app still starts too early
Possible causes
- depends_on only controls start order, not readiness
- No healthcheck is defined for the dependency
- The healthcheck tests the process but not the actual service
- The application has no startup retry or connection timeout
- The database initialization runs after the port opens
Diagnosis steps
Check container state and logs
docker compose ps && docker compose logs app dbInterpretation
The app's connection error and the database's initialization logs show whether the dependency was ready.
Inspect health status
docker inspect --format '{{json .State.Health}}' CONTAINERInterpretation
A running container without a healthcheck cannot signal readiness; a failing healthcheck shows the service check result.
Test readiness from inside the network
docker compose exec app sh -c 'nc -z db 5432; echo $?'Interpretation
A refused connection at app startup confirms a race, not a configuration typo.
Root cause
The application started before its dependency was ready. depends_on changes start order but does not wait for the dependency to accept connections. Readiness must be modeled with healthchecks and application retries.
Fix
Add a meaningful healthcheck to the dependency, use depends_on with condition: service_healthy, and add bounded connection retries in the application. Do not rely on a fixed sleep.
Risk note
A long fixed sleep slows every start and still fails on a slow machine. A healthcheck that only checks the process can pass before the database accepts queries.
Verify the fix
Run docker compose down && docker compose up and confirm the app waits for the healthy dependency. Stop the dependency and verify the app retries or fails with a clear error rather than crashing. Repeat on a cold volume.
Start order is not readiness
depends_on starts the dependency container before the app, but the dependency may still be initializing. PostgreSQL, for example, can open a port while it is still recovering or creating the database. The app connects immediately and gets a refused connection or an authentication error.
services:
db:
image: postgres:15-alpine
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app -d app"]
interval: 2s
timeout: 3s
retries: 20
app:
build: .
depends_on:
db:
condition: service_healthy
Make the healthcheck meaningful
A healthcheck should test the service the app needs, not only that the process exists. pg_isready checks the database accepts connections. A Redis PING or an HTTP readiness endpoint is similarly better than a TCP port check. Set an interval and retries that fit the slowest expected startup.
Retry in the application too
Even with a healthcheck, a restart or a network blip can happen after startup. The application should use bounded connection retries with backoff and a clear failure message. This protects against orchestrator differences and makes local and production behavior consistent.
Verify from a cold start
Run docker compose down -v and docker compose up, then repeat without the volume. Confirm the app waits for health and starts successfully. Stop the database during startup and verify the app retries or fails clearly instead of crashing with an opaque error.
Fix and verify
Fix the startup race with a real healthcheck on the dependency, condition: service_healthy, and bounded connection retries in the application. Verify by starting from a cold volume, by stopping the dependency during boot, and by confirming the app either waits successfully or reports a clear retryable error. A fixed sleep is not a fix; it only changes the timing of the same race.