How to manage service dependencies in Docker Compose?
Service dependencies in Docker Compose are about more than start order. The dep being "started" rarely means "ready to accept connections". The right pattern: depends_on: condition: service_healthy with a healthcheck on the dependency.
Theory
TL;DR
- Plain
depends_on: [db]= start order only. Compose startsdbfirst, thenapi. Does NOT wait fordbto be ready. depends_on: db: condition: service_healthy= waits untildb's healthcheck passes. This is what you want for production.- Three conditions:
service_started,service_healthy,service_completed_successfully(for one-off init containers). - Without depends_on, services start in parallel. With it, Compose enforces the order.
- For non-Compose orchestrators, the same idea exists: K8s
initContainers, Swarm dependency-via-healthcheck-on-routes.
Quick example
# compose.yaml
services:
api:
image: myapp
environment:
DATABASE_URL: postgres://postgres:dev@db:5432/app
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
migrate:
condition: service_completed_successfully
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: dev
POSTGRES_DB: app
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
retries: 5
start_period: 5s
redis:
image: redis:7
migrate:
image: mymigrator
depends_on:
db:
condition: service_healthy
command: ["npm", "run", "migrate"]$ docker compose up -d
[+] Running 4/4
✔ Container db Healthy 2.3s
✔ Container redis Started 2.4s
✔ Container migrate Exited 8.7s # ran, succeeded, exited 0
✔ Container api Started 8.8s # waited for migrate to finishFour services, three different dependency conditions, perfect ordering: db ready → migrate runs → migrate succeeds → api starts (with redis already up).
The three conditions
service_started (the default)
depends_on:
- db
# OR explicitly:
depends_on:
db:
condition: service_startedWaits until the container starts. Does NOT wait for the app inside to be ready. Almost never what you want.
service_healthy
depends_on:
db:
condition: service_healthyWaits until the dep's healthcheck passes. Requires db to have a healthcheck: defined. The right default for runtime dependencies.
If db has no healthcheck, this fails the dependent's start (Compose errors out with "service has no healthcheck").
service_completed_successfully
depends_on:
migrate:
condition: service_completed_successfullyWaits for the dep to exit with code 0. Used for one-shot init/migration containers — they run, do their job, exit, then the main service starts.
Why depends_on alone is not enough
# WRONG: only orders starts; api starts while postgres is still booting
services:
api:
depends_on:
- db
db:
image: postgres:16$ docker compose up
Container db Started
Container api Started # but db needs ~3 seconds to actually accept connections
# api hits db → Connection refused → api crashes or retriesMost databases need 1-5 seconds after "started" before they accept queries. depends_on alone does not know this. The healthcheck does.
App-level retry as a safety net
Even with service_healthy, transient db disconnects happen at runtime. Production apps should retry with backoff:
// Pseudo-code
async function connectDB() {
for (let i = 0; i < 10; i++) {
try { return await pg.connect(...); }
catch (e) {
console.log(`db retry ${i+1}/10: ${e.message}`);
await sleep(1000 * (i + 1));
}
}
throw new Error('db unreachable after 10 attempts');
}App-level retry + Compose service_healthy together = robust startup.
Common mistakes
Using plain depends_on for a real dependency
# WRONG
depends_on: [db]Starts api before db is ready. App fails on first connection.
Forgetting the dep needs a healthcheck
services:
api:
depends_on:
db:
condition: service_healthy
db:
image: postgres:16
# no healthcheck:ERROR: ...service "db" has no healthcheck defined
Fix: add a healthcheck: block to db.
Wait-for scripts are not always needed
The pre-condition: service_healthy era required scripts like wait-for-it or dockerize:
CMD ["./wait-for-it.sh", "db:5432", "--", "node", "server.js"]With Compose v3+ and service_healthy, you do not need these for inter-service start order. They are still useful for external dependencies (cloud DBs reached over the network).
Cyclic dependencies
services:
a:
depends_on: [b]
b:
depends_on: [a]ERROR: dependency cycle detected
Compose refuses. Real dependency graphs must be acyclic.
Dependency only honored at startup, not at restart of a single service
$ docker compose restart api
# Restarts only api. Does NOT verify db's health again.The healthcheck-gated dependency is enforced during up. restart is a quicker operation. Usually fine; rarely a foot-gun.
Real-world patterns
Pattern: db + migrator + app
services:
db:
image: postgres:16
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 3s
retries: 10
migrate:
image: myapp
command: ["npm", "run", "migrate"]
depends_on:
db:
condition: service_healthy
app:
image: myapp
depends_on:
migrate:
condition: service_completed_successfully
db:
condition: service_healthydb healthy → migrate runs → migrate exits 0 → app starts. Three conditions, deterministic startup.
Pattern: web + api + db
services:
db:
image: postgres:16
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
api:
build: ./api
depends_on:
db:
condition: service_healthy
healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://localhost:3000/health"]
web:
image: nginx
depends_on:
api:
condition: service_healthy
ports: ["80:80"]Three-tier dependency chain, each gate is healthcheck-driven.
Follow-up questions
Q: Does depends_on work across multiple Compose files?
A: Yes — depends_on resolves within the merged result of all -f files. References must point at services defined somewhere in the merged config.
Q: Can a service depend on multiple services with mixed conditions?
A: Yes. Use the map form to set per-dep conditions:
depends_on:
db: { condition: service_healthy }
redis: { condition: service_started }
migrate: { condition: service_completed_successfully }Q: Does depends_on carry over to docker compose run?
A: Yes — Compose starts deps when you do docker compose run --rm api .... Disable with --no-deps.
Q: What if my dep is an external service (like AWS RDS)?
A: Compose cannot health-check it. Either add a small wrapper container with a healthcheck that probes the external service, or rely on app-level retry.
Q: (Senior) How does this map to Kubernetes?
A: Kubernetes does not have depends_on for pods directly. The equivalents: initContainers (run before main containers, must succeed), readiness probes (gate traffic to the pod), and external orchestration tools (Argo Workflows, etc.) for complex dependencies. Helm charts often hardcode startup-order assumptions using deployments + services + readiness probes; the explicit dependency graph is a Compose-flavored convention.
Examples
Real prod-shape Compose
services:
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: ${DB_PASSWORD:-dev}
POSTGRES_DB: app
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres -d app"]
interval: 3s
timeout: 2s
retries: 10
start_period: 5s
restart: unless-stopped
migrate:
image: myapp:${TAG:-latest}
command: ["npm", "run", "migrate"]
depends_on:
db:
condition: service_healthy
environment:
DATABASE_URL: postgres://postgres:${DB_PASSWORD:-dev}@db:5432/app
restart: "no"
api:
image: myapp:${TAG:-latest}
depends_on:
migrate:
condition: service_completed_successfully
db:
condition: service_healthy
environment:
DATABASE_URL: postgres://postgres:${DB_PASSWORD:-dev}@db:5432/app
healthcheck:
test: ["CMD-SHELL", "wget -q --spider http://localhost:3000/health"]
interval: 30s
retries: 3
start_period: 10s
restart: unless-stopped
web:
image: nginx:1.27-alpine
depends_on:
api:
condition: service_healthy
ports: ["80:80"]
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
restart: unless-stopped
volumes:
pgdata:Four-service stack, deterministic startup, every service has its restart policy and healthcheck, migrations gate api.
Init container pattern (one-shot setup)
services:
db-init:
image: postgres:16
command: ["sh", "-c", "echo 'CREATE EXTENSION IF NOT EXISTS pg_stat_statements;' | psql -h db -U postgres"]
depends_on:
db:
condition: service_healthy
restart: "no"
app:
image: myapp
depends_on:
db-init:
condition: service_completed_successfullydb-init is a one-shot job. It runs after db is healthy, performs setup, exits 0, then app starts.
When the dep is external
services:
db-check:
image: alpine
command: ["sh", "-c", "until nc -z external-db.aws.com 5432; do sleep 1; done"]
restart: "no"
api:
image: myapp
depends_on:
db-check:
condition: service_completed_successfullyA tiny check container that waits for an external dep, then exits. The api is gated on it.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.
Comments
No comments yet