Dependency Order and Healthchecks
In a multi-service stack, "container started" does not mean "service ready." If your API tries to connect to PostgreSQL before it has finished initializing, you get a crash. Compose gives you tools to handle this properly.
The Problem
depends_on Without Health Checks (Not Enough)
By default, depends_on only controls startup order, not readiness:
services:
api:
image: my-api:1.0.0
depends_on:
- db # Only waits for db container to START, not be READY
db:
image: postgres:16
The API container starts as soon as the db container has been created -- but PostgreSQL may still be initializing.
depends_on With Health Checks (The Right Way)
Add a health check to the dependency and use condition: service_healthy:
services:
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: ${DB_PASSWORD}
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5
start_period: 20s
api:
image: my-api:1.0.0
depends_on:
db:
condition: service_healthy
Now the API will not start until PostgreSQL is actually accepting connections.
Health Check Parameters
| Parameter | What It Does | Recommendation |
|---|---|---|
test | Command that returns 0 (healthy) or 1 (unhealthy) | Use the service's native check tool |
interval | Time between checks | 10-30s for most services |
timeout | Max time for a single check | 3-5s |
retries | Failures before marking unhealthy | 3-5 |
start_period | Grace period during startup (failures don't count) | Set to expected initialization time |
Common Health Check Commands
| Service | Health Check |
|---|---|
| PostgreSQL | pg_isready -U postgres |
| MySQL | mysqladmin ping -h localhost |
| Redis | redis-cli ping |
| HTTP app | curl -f http://localhost:8080/health |
| MongoDB | mongosh --eval "db.runCommand('ping')" |
| RabbitMQ | rabbitmq-diagnostics -q check_running |
Dependency Graph
In complex stacks, draw the dependency graph to avoid circular dependencies:
services:
proxy:
depends_on:
api: { condition: service_healthy }
api:
depends_on:
db: { condition: service_healthy }
cache: { condition: service_healthy }
worker:
depends_on:
queue: { condition: service_healthy }
db: { condition: service_healthy }
App-Level Retries Are Still Required
Even with health checks, transient failures can happen. Your application should implement connection retry logic:
# Python example: retry database connection
import time, psycopg2
for attempt in range(10):
try:
conn = psycopg2.connect("host=db dbname=myapp user=postgres")
break
except psycopg2.OperationalError:
print(f"DB not ready, retry {attempt + 1}/10...")
time.sleep(2)
Health checks solve startup sequencing. Retries solve transient failures.
Verifying Health Status
# Check health status of all services
docker compose ps
# Detailed health info for a specific container
docker inspect --format='{{json .State.Health}}' myproject-db-1 | python3 -m json.tool
Key Takeaways
depends_onalone only controls start order, not readiness. Always addcondition: service_healthy.- Define health checks for every service that other services depend on (databases, caches, queues).
- Use
start_periodto give slow-starting services time to initialize without being marked unhealthy. - Health checks solve startup sequencing. App-level retries solve transient runtime failures. You need both.
- Draw your dependency graph to catch circular dependencies early.
What's Next
- Continue to Multi-Environment Overrides to manage dev, staging, and production configurations.