Depends On in Docker Compose Does Not Mean What You Think It Means
by Arif Ikhsanudin, Backend Developer
The depends_on that doesn't actually help
You added depends_on: db to your application service in Docker Compose. You thought this meant "don't start the app until the database is ready." You still get connection errors on first startup. The database container starts before the app container, but the PostgreSQL server inside it isn't ready to accept connections when your application tries to connect.
The confusion is between container start order and service readiness. depends_on without conditions only controls the former.
What depends_on without conditions actually does
services:
app:
depends_on:
- db
db:
image: postgres:16-alpine
This tells Compose: start db before starting app. The db container will be in running state when app starts. That's it. It says nothing about whether PostgreSQL inside the container is initialized, has created the data directory, or is accepting connections.
PostgreSQL's startup sequence:
- Container starts → Docker reports
running postgresprocess initializes data directory (if first run)- PostgreSQL applies configuration files
- PostgreSQL starts listening on port 5432
- PostgreSQL is ready to accept connections
Steps 2–5 take 5–30 seconds depending on hardware and whether it's a first-run initialization. Your application starts immediately after step 1.
depends_on with conditions: what actually works
Compose v2 (the current version) introduced condition to depends_on:
services:
app:
depends_on:
db:
condition: service_healthy
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: appuser
POSTGRES_DB: myapp
POSTGRES_PASSWORD: secret
healthcheck:
test: ["CMD-SHELL", "pg_isready -U appuser -d myapp"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
Now depends_on: db: condition: service_healthy means: don't start app until db reports healthy. Compose polls the db health check and waits until it passes before starting app.
Three conditions are available:
service_started: The default whenconditionis omitted. Container is running. Same as the old baredepends_on: dbbehavior.service_healthy: Container is running AND health check is passing. Requires ahealthcheckdirective on the dependency.service_completed_successfully: Container has exited with code 0. For one-shot services like database migrations.
The health check is not optional with service_healthy
If you use condition: service_healthy without a healthcheck on the dependency, Compose will wait indefinitely because the dependency will never be considered healthy. Or in some Compose versions it throws an error immediately.
Always pair condition: service_healthy with an appropriate health check:
PostgreSQL:
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-postgres}"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
MySQL:
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "--silent"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
Redis:
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 10
RabbitMQ:
healthcheck:
test: ["CMD", "rabbitmq-diagnostics", "ping"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
HTTP service:
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:8080/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 60s
service_completed_successfully for migrations
A pattern that comes up in Spring Boot and other JVM apps: running database migrations as a separate step before the application starts.
services:
db:
image: postgres:16-alpine
# ... healthcheck as above
migrate:
image: flyway/flyway:10-alpine
command: >
-url=jdbc:postgresql://db:5432/${DB_NAME}
-user=${DB_USER}
-password=${DB_PASSWORD}
migrate
volumes:
- ./src/main/resources/db/migration:/flyway/sql:ro
depends_on:
db:
condition: service_healthy
app:
build: .
depends_on:
db:
condition: service_healthy
migrate:
condition: service_completed_successfully
The app waits for both the database to be healthy and the migration container to have exited with code 0. If migrations fail (exit code non-zero), Compose doesn't start the app, and the compose up command exits with an error. This is the correct behavior.
The startup_completed condition: not what you might need
There's a fourth condition worth knowing about: service_started is not the same as "the service inside the container has fully started." Even with health checks, there can be a brief period after the health check passes where the service is still warming up. For most use cases service_healthy is sufficient — the health check is designed to pass when the service is ready.
If your application has its own startup health check endpoint that returns 200 only when the application is fully initialized (connection pools established, caches warmed), use that in your health check. Don't use a basic TCP connectivity check as a proxy for application readiness if you can check application readiness directly.
What depends_on doesn't solve
depends_on only helps at initial startup. If a dependency becomes unavailable after the app has started — database restarts, Redis is OOMKilled, a downstream service crashes — depends_on does nothing. Your application needs to handle transient failures with retry logic and circuit breaking.
For database connections, use a connection pool with retry configuration:
# Spring Boot + HikariCP
SPRING_DATASOURCE_HIKARI_CONNECTION_TIMEOUT: 30000
SPRING_DATASOURCE_HIKARI_INITIALIZATION_FAIL_TIMEOUT: 60000
initialization-fail-timeout=-1 tells HikariCP to retry indefinitely during startup rather than failing immediately if the database isn't available. This provides an additional safety net beyond depends_on ordering — even if your Compose health check configuration isn't quite right, your connection pool will retry.
The version of Compose you're using matters
condition on depends_on is a Compose v2 feature. If you're using the old docker-compose (v1) binary — identifiable by the version: "3" or version: "2" at the top of your Compose file — the condition syntax may not work or may be silently ignored.
Check which version you're using:
docker compose version # v2 — built into Docker
docker-compose version # v1 — separate Python binary (deprecated)
If you're on v1, migrate to docker compose (v2). V1 reached end-of-life in 2023.
The version: key at the top of Compose files is also deprecated in Compose v2 and can be removed. It's ignored by current versions.