How do you express a dependency between services in Compose, and what are its limitations?
Quick Answer
depends_on controls startup order — Compose starts a dependency's container before the dependent service's container. By default, this only waits for the dependency's container to start, not for the application inside it to actually be ready to accept connections — a real and common source of race conditions (e.g., an API container starting before its database has actually finished initializing). Adding condition: service_healthy (requiring the dependency to define a HEALTHCHECK) closes this gap, making Compose wait for genuine readiness, not just process start.
Detailed Answer
The basic (and incomplete) version
services:
db:
image: postgres:16
api:
image: myapi:1.0
depends_on:
- db
docker compose up will start db's container before starting api's container. But critically, this only guarantees that db's container process has started — not that PostgreSQL inside it has actually finished its own initialization and is ready to accept connections. Database engines (and many other services) often take a real, non-trivial amount of time after their process starts before they're genuinely ready to serve requests.
The resulting race condition
1. docker compose up starts db's container
2. Compose immediately proceeds to start api's container (db's PROCESS has started)
3. api's application code tries to connect to the database...
4. ...but Postgres inside db is STILL initializing, not yet accepting connections
5. api's connection attempt fails, possibly crashing api on startup
This is a very common, easy-to-hit real-world Compose bug. Everything looks correctly ordered, since depends_on: [db] is right there in the file. But the actual application-level readiness gap between "container started" and "service ready" isn't accounted for at all by plain depends_on.
The fix: condition: service_healthy
services:
db:
image: postgres:16
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 5
api:
image: myapi:1.0
depends_on:
db:
condition: service_healthy
This changes the guarantee meaningfully: api's container now won't even be started until db's own HEALTHCHECK (see the lifecycle topic's question) actually reports healthy — genuinely waiting for readiness, not just process start. This requires the dependency (db) to actually define a HEALTHCHECK in the first place; condition: service_healthy has nothing to check against otherwise.
Other available conditions
depends_on:
db:
condition: service_started # the default -- just waits for the container to start
migrations:
condition: service_completed_successfully # waits for a one-off task container to EXIT successfully
service_completed_successfully is specifically useful for a one-off setup task — for example, running database migrations as a separate service that's expected to run once and exit. A dependent service can wait for this task to fully finish, rather than waiting for a long-running service to become healthy.
What depends_on still doesn't solve, even with service_healthy
Even with a proper healthcheck-gated dependency, Compose's depends_on only governs the initial startup sequence. It says nothing about what happens if db later crashes and restarts while api is already running. api's application code still needs its own resilience — retry logic, connection pooling with reconnection — to handle a dependency becoming temporarily unavailable after both services are already up and running. depends_on is purely a startup-ordering tool, not an ongoing dependency-health guarantee for the whole lifetime of the application.