Depends On & Startup Order #
When you have many services, startup order matters. The database must be ready before the application tries to connect. The cache must be ready before workers start pulling messages. Docker Compose provides the depends_on field to manage this order.
But depends_on has a limitation that often causes bugs: it only waits for containers to start, not for services to be ready to accept requests. To truly wait for readiness, you need a combination of healthchecks + condition.
This article covers all of that in depth, complete with common dependency patterns.
depends_on: Basic Syntax #
The depends_on field sits under a service and contains a list of other services that must start first.
services:
web:
depends_on:
- api
api:
depends_on:
- db
- cache
db:
image: postgres:16-alpine
cache:
image: redis:7-alpine
In the example above, db and cache start first (in parallel), then api starts, then web. This order is deterministic.
depends_on can also be written with map syntax for more explicitness:
services:
api:
depends_on:
db:
condition: service_started
cache:
condition: service_started
condition can be:
service_started(default) — the container has started, but may not be ready.service_healthy— the healthcheck passed.service_completed_successfully— the container exited with code 0 (for one-off jobs).
The Problem: depends_on Doesn’t Wait for Readiness #
The biggest weakness of default depends_on is that it only waits for the container to start, not for the service to be ready. For services needing long initialization (databases loading schemas, JVM warm-up, lazy initialization), depends_on alone isn’t enough.
Example problem:
services:
api:
build: ./api
depends_on:
- db
db:
image: postgres:16-alpine
# The database needs a few seconds to accept connections
When docker compose up runs, the db container starts (the postgres process begins). Compose immediately starts api. But postgres may not be ready to accept connections yet — the database hasn’t initialized its cluster, not ready for queries. The API tries to connect, fails, crashes.
This is a very common bug among Docker Compose beginners.
The Solution: Healthcheck + Condition: service_healthy #
To truly wait for readiness, add a healthcheck to the dependency service, then use condition: service_healthy.
services:
api:
build: ./api
depends_on:
db:
condition: service_healthy
db:
image: postgres:16-alpine
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
api now won’t start until db’s healthcheck passes. Postgres ready to accept connections = the API can start safely.
Healthchecks in Docker Compose #
A healthcheck is a command run periodically inside the container to determine whether the service is healthy.
services:
db:
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"] # the test command
interval: 10s # every how many seconds
timeout: 5s # per-test timeout
retries: 5 # failures before unhealthy
start_period: 30s # initial grace period
Healthcheck fields:
| Field | Default | Description |
|---|---|---|
test | (required) | The health-checking command |
interval | 30s | Interval between tests |
timeout | 30s | Per-test timeout |
retries | 3 | Failures before considered unhealthy |
start_period | 0s | Initial grace period (failed tests ignored) |
test can be:
["CMD", "command", "arg1", "arg2"]— exec form["CMD-SHELL", "shell command"]— shell form["NONE"]— disable an inherited healthcheck from the image
Example Healthchecks per Service #
Postgres:
db:
image: postgres:16-alpine
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres -d myapp"]
interval: 10s
timeout: 5s
retries: 5
Redis:
cache:
image: redis:7-alpine
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 3
MySQL:
db:
image: mysql:8
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 10s
timeout: 5s
retries: 5
MongoDB:
db:
image: mongo:7
healthcheck:
test: ["CMD", "mongo", "--eval", "db.runCommand({ping: 1})"]
interval: 10s
timeout: 5s
retries: 5
RabbitMQ:
mq:
image: rabbitmq:3-management
healthcheck:
test: ["CMD", "rabbitmq-diagnostics", "ping"]
interval: 30s
timeout: 10s
retries: 3
start_period: 60s
Custom HTTP API:
api:
build: .
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:8080/health || exit 1"]
interval: 30s
timeout: 10s
retries: 3
start_period: 30s
A good healthcheck tests whether the service can accept requests from external clients, not just whether the process is running. For web services, check the/healthor/readyendpoint. For databases, check whether queries can be accepted.
Common Dependency Patterns #
API + Database #
The most classic pattern:
services:
api:
build: ./api
depends_on:
db:
condition: service_healthy
environment:
- DATABASE_URL=postgres://app:pass@db:5432/myapp
db:
image: postgres:16-alpine
environment:
- POSTGRES_USER=app
- POSTGRES_PASSWORD=secret
- POSTGRES_DB=myapp
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app -d myapp"]
interval: 10s
timeout: 5s
retries: 5
volumes:
- db-data:/var/lib/postgresql/data
volumes:
db-data:
Frontend + API + Multiple Backends #
services:
web:
image: nginx
depends_on:
- api
api:
build: ./api
depends_on:
db:
condition: service_healthy
cache:
condition: service_healthy
mq:
condition: service_healthy
db:
image: postgres:16-alpine
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5
cache:
image: redis:7-alpine
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 3
mq:
image: rabbitmq:3-management
healthcheck:
test: ["CMD", "rabbitmq-diagnostics", "ping"]
interval: 30s
timeout: 10s
retries: 3
start_period: 60s
Worker + Queue #
services:
worker:
build: ./worker
depends_on:
rabbitmq:
condition: service_healthy
redis:
condition: service_healthy
environment:
- QUEUE_URL=amqp://guest:guest@rabbitmq:5672
- REDIS_URL=redis://redis:6379
rabbitmq:
image: rabbitmq:3-management
healthcheck:
test: ["CMD", "rabbitmq-diagnostics", "ping"]
interval: 30s
timeout: 10s
retries: 3
redis:
image: redis:7-alpine
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 3
The Init Container Pattern #
To set up data before the main service starts, use an init container:
services:
app:
build: .
depends_on:
init:
condition: service_completed_successfully
volumes:
- data:/app/data
init:
image: alpine
volumes:
- data:/app/data
command: sh -c "echo '$(date)' > /app/data/init.txt && echo 'init complete'"
restart: "no"
The init container runs once, exits. app starts after init exits with code 0.
Startup Order vs Runtime #
depends_on manages startup order, not runtime behavior. Once all services are running, dependencies between them are handled by the application protocol itself (retries, circuit breakers, etc.).
Example: api depends on db at startup, but at runtime, api must handle db restarts or temporary unavailability. Healthchecks help, but retry logic in the application also matters.
# Example retry logic in Python
import time
import psycopg2
def connect_with_retry(max_retries=5, delay=2):
for attempt in range(max_retries):
try:
return psycopg2.connect(DATABASE_URL)
except psycopg2.OperationalError:
if attempt < max_retries - 1:
time.sleep(delay)
delay *= 2 # exponential backoff
else:
raise
Parallel Startup #
Services that don’t depends_on each other start in parallel. This is good for performance.
services:
cache:
image: redis:7-alpine
mq:
image: rabbitmq:3-management
db:
image: postgres:16-alpine
cache, mq, and db above start in parallel because they don’t depend on each other.
But services with depends_on in the same chain start sequentially:
services:
web: # starts 4th
depends_on: [api]
api: # starts 3rd
depends_on: [db, cache]
db: # starts 1st
image: postgres
cache: # starts 2nd (in parallel with db)
image: redis
Docker Compose tries to start as many services in parallel as possible while respecting dependencies.
Best Practices #
Always Use Healthchecks for Dependencies #
For services with dependencies, always add healthchecks. Without them, depends_on alone is unreliable.
# MANDATORY for services with dependents
db:
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5
Tune Healthcheck Intervals and Retries #
The default values (30s interval, 3 retries) may be too slow or too fast for your case.
- Fast-starting services (Redis, nginx): short intervals (5-10s), low retries (3).
- Slow-starting services (large databases, JVM): long intervals (30s+), high retries (5-10), long start_periods (30-60s).
Avoid Circular Dependencies #
Service A depends on B, B depends on A → circular dependency → docker compose error.
Split the dependency chain.
# AVOID
services:
api:
depends_on: [worker]
worker:
depends_on: [api]
# CORRECT
services:
api:
depends_on: [queue]
worker:
depends_on: [queue]
Use Restart Policies for Resilience #
For restartable services, set restart: on-failure or unless-stopped. Crashed containers restart automatically.
services:
worker:
restart: unless-stopped
depends_on:
queue:
condition: service_healthy
Logging for Debugging #
When depends_on doesn’t work as expected, check the dependent service’s logs.
docker compose logs api
docker compose logs --tail 100 web
Errors like “connection refused” or “no such host” usually mean a dependent started before its dependency was ready.
Pattern Recap #
Choose a pattern by dependency complexity:
| Pattern | Use when |
|---|---|
depends_on: [service] | The dependent can retry, the dependency starts fast |
depends_on: { x: { condition: service_healthy } } | The dependent must wait for the dependency to be ready |
| Init container | Data setup needed before the main service |
No depends_on | Independent services, no ordering needed |
Common Pitfalls #
1. Forgetting the Healthcheck #
The most common. Without a healthcheck, condition: service_healthy is meaningless.
# WRONG
services:
api:
depends_on:
db:
condition: service_healthy # useless without a healthcheck
db:
image: postgres
# healthcheck missing
2. Healthchecks Too Strict or Too Loose #
Too strict (tests failing early): container restart loops. Too loose (tests always pass): unhealthy containers go undetected.
Tune with start_period for a grace period.
3. Assuming depends_on Means a Runtime Dependency
#
depends_on is only for startup. Runtime needs other handling (retries, circuit breakers).
4. Not Testing Restart Scenarios #
docker compose up once is easy. Also test: stop db after the stack is running, then start it again. Does api reconnect correctly?
A Complete Example: A Microservice Stack #
services:
nginx:
image: nginx:alpine
ports:
- "80:80"
depends_on:
web:
condition: service_healthy
networks:
- frontend
restart: unless-stopped
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "http://localhost/health"]
interval: 30s
timeout: 10s
retries: 3
web:
build: ./web
depends_on:
api:
condition: service_healthy
networks:
- frontend
- backend
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "http://localhost:3000"]
interval: 30s
timeout: 10s
retries: 3
restart: unless-stopped
api:
build: ./api
depends_on:
db:
condition: service_healthy
cache:
condition: service_healthy
mq:
condition: service_healthy
networks:
- backend
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:8080/health || exit 1"]
interval: 30s
timeout: 10s
retries: 3
restart: unless-stopped
worker:
build: ./worker
depends_on:
api:
condition: service_healthy
mq:
condition: service_healthy
networks:
- backend
restart: unless-stopped
db:
image: postgres:16-alpine
environment:
- POSTGRES_USER=app
- POSTGRES_PASSWORD=secret
- POSTGRES_DB=myapp
volumes:
- db-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app -d myapp"]
interval: 10s
timeout: 5s
retries: 5
networks:
- backend
restart: unless-stopped
cache:
image: redis:7-alpine
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 3
networks:
- backend
restart: unless-stopped
mq:
image: rabbitmq:3-management
healthcheck:
test: ["CMD", "rabbitmq-diagnostics", "ping"]
interval: 30s
timeout: 10s
retries: 3
start_period: 60s
networks:
- backend
restart: unless-stopped
volumes:
db-data:
networks:
frontend:
backend:
This stack has 7 services with dependencies:
nginxdepends onwebwebdepends onapiapidepends ondb,cache,mqworkerdepends onapi,mq
All services have healthchecks. Container restart policy is unless-stopped except for those needing graceful termination.
Startup order (parallelism maximized):
db,cache,mqstart in parallel- Once all three are healthy,
apiandworkerstart in parallel - Once
apiis healthy,webstarts - Once
webis healthy,nginxstarts
Total startup time: the sum of healthcheck intervals + start_periods, not a sum from zero. Compose waits until all dependencies are ready in parallel.
Summary #
depends_onmanages startup order, but by default only waits for containers to start, not for services to be ready.- To truly wait for readiness, add a healthcheck to the dependency service +
condition: service_healthyindepends_on.- Healthchecks must genuinely test whether the service can accept requests, not just whether the process is running.
- Startup order ≠ runtime dependency. Runtime needs retry logic in the application.
- Parallel startup — services without
depends_onon each other start in parallel. Compose maximizes parallelism while respecting chains.- Avoid circular dependencies — split the dependency chain.
- Tune healthchecks per service: intervals/retries matching service characteristics (startup time, response time).
- Restart policies for resilience — crashed services auto-restart, as long as dependencies stay healthy.
- Debug with
docker compose logs— errors like “connection refused” or “no such host” usually mean a dependent started before its dependency was ready.- Init containers for data setup before the main service starts, with
condition: service_completed_successfully.