Fix Docker Compose depends_on Not Working
Your app crashes on startup because depends_on only controls start order — it does not wait for services to be ready. Here is why that happens and how to fix it properly.
- 1. Add a
healthcheckto the service your app depends on - 2. Set
condition: service_healthyin yourdepends_on - 3. As a fallback, add retry logic with exponential backoff in your app
The Problem
You set up depends_on in your docker-compose.yml, but your app still crashes because the database is not ready yet. The error message varies depending on your stack:
# Node.js Error: connect ECONNREFUSED 127.0.0.1:5432 # Python psycopg.OperationalError: connection refused # Java org.postgresql.util.PSQLException: Connection refused # Go dial tcp 127.0.0.1:5432: connect: connection refused
The container for PostgreSQL started, but PostgreSQL itself is still initializing. Your app tries to connect immediately and gets rejected.
Why depends_on Doesn't Wait
There is a critical difference between a container being started and a service being ready:
depends_on waits for by default.# This ONLY waits for the container to start: depends_on: - postgres # Timeline: # 0.0s → postgres container starts # 0.1s → your app container starts (depends_on satisfied!) # 0.1s → your app tries to connect → ECONNREFUSED # 2.5s → PostgreSQL finishes init and is ready (too late!)
Solution 1: healthcheck + condition: service_healthy
The modern and recommended approach. Add a healthcheck to the dependency service, then use condition: service_healthy in depends_on. Docker Compose will wait until the healthcheck passes before starting the dependent service.
# docker-compose.yml
services:
postgres:
image: postgres:16
environment:
POSTGRES_USER: myuser
POSTGRES_PASSWORD: mypassword
POSTGRES_DB: mydb
healthcheck:
test: ["CMD-SHELL", "pg_isready -U myuser -d mydb"]
interval: 5s
timeout: 5s
retries: 5
start_period: 10s
redis:
image: redis:7
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 5s
retries: 5
start_period: 5s
web:
build: .
ports:
- "3000:3000"
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
environment:
DATABASE_URL: postgresql://myuser:mypassword@postgres:5432/mydb
REDIS_URL: redis://redis:6379How healthcheck parameters work:
| Parameter | Meaning | Default |
|---|---|---|
| test | Command to check if the service is healthy | — |
| interval | Time between health checks | 30s |
| timeout | Max time for a single check to complete | 30s |
| retries | Consecutive failures before marking unhealthy | 3 |
| start_period | Grace period before health checks count | 0s |
Solution 2: Add Retry Logic to Your Application
Even with healthchecks, it is good practice to add retry logic in your application code. This handles edge cases like temporary network blips or the service becoming unavailable after startup.
// Node.js / TypeScript — retry with exponential backoff
async function connectWithRetry(
connectFn: () => Promise<void>,
maxRetries = 10,
baseDelay = 1000,
): Promise<void> {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
await connectFn();
console.log(`Connected on attempt ${attempt}`);
return;
} catch (error) {
if (attempt === maxRetries) {
throw new Error(
`Failed to connect after ${maxRetries} attempts: ${error}`
);
}
const delay = baseDelay * Math.pow(2, attempt - 1);
console.warn(
`Attempt ${attempt} failed, retrying in ${delay}ms...`
);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
// Usage:
await connectWithRetry(async () => {
const pool = new Pool({ connectionString: DATABASE_URL });
await pool.query('SELECT 1');
});# Python — retry with tenacity
from tenacity import retry, stop_after_attempt, wait_exponential
import psycopg
@retry(
stop=stop_after_attempt(10),
wait=wait_exponential(multiplier=1, min=1, max=30),
)
def connect_to_db():
conn = psycopg.connect(DATABASE_URL)
conn.execute("SELECT 1")
return connSolution 3: Wait-for-it Script (Legacy)
Before Docker Compose supported healthcheck conditions, the common workaround was to use a wait-for-it script in the container entrypoint. This still works but is considered a legacy approach.
# docker-compose.yml (legacy approach)
services:
postgres:
image: postgres:16
environment:
POSTGRES_USER: myuser
POSTGRES_PASSWORD: mypassword
web:
build: .
command: >
sh -c "
wget -qO- https://raw.githubusercontent.com/eficode/wait-for-it/master/wait-for-it.sh > /tmp/wait-for-it.sh &&
chmod +x /tmp/wait-for-it.sh &&
/tmp/wait-for-it.sh postgres:5432 --timeout=60 --strict &&
node server.js
"
depends_on:
- postgresWhy this is legacy: It only checks if the TCP port is open — not whether the service is actually ready. A port can be open during initialization while the service is not yet accepting queries. The healthcheck approach is more reliable.
Healthcheck Commands for Common Services
| Service | Healthcheck Command |
|---|---|
| PostgreSQL | ["CMD-SHELL", "pg_isready -U $POSTGRES_USER"] |
| MySQL | ["CMD", "mysqladmin", "ping", "-h", "localhost"] |
| Redis | ["CMD", "redis-cli", "ping"] |
| MongoDB | ["CMD", "mongosh", "--eval", "db.adminCommand('ping')"] |
| Elasticsearch | ["CMD-SHELL", "curl -f http://localhost:9200/_cluster/health"] |
Debugging Tips
Check container health status
docker compose ps # Look at the STATUS column: # Name Command Status # postgres docker-entrypoint… Up 30s (healthy) # redis docker-entrypoint… Up 30s (health: starting) # web node server.js Up 25s # "healthy" = healthcheck passed # "health: starting" = still in start_period or first checks # "unhealthy" = healthcheck failed retries
View healthcheck logs
# Inspect health check results
docker inspect --format='{{json .State.Health}}' <container>
# Pretty-printed:
docker inspect <container> | jq '.[0].State.Health'
# View health check output (last 5 entries)
docker inspect --format='{{range .State.Health.Log}}{{.Output}}{{end}}' <container>Force rebuild after changing healthcheck
# If you changed the healthcheck, rebuild and restart: docker compose down docker compose up --build -d # Or just restart without rebuild: docker compose up -d --force-recreate
Common Mistakes
Frequently Asked Questions
Why does Docker Compose depends_on not wait for the service to be ready?
depends_on only controls the order in which containers are started, not when they are ready. A container can be running while the service inside (like PostgreSQL) is still initializing. To wait for readiness, use healthcheck with condition: service_healthy.
How do I make Docker Compose wait for a database to be ready?
Add a healthcheck to the database service and use condition: service_healthy in depends_on. For PostgreSQL: healthcheck with pg_isready. For MySQL: healthcheck with mysqladmin ping. Then set depends_on with condition: service_healthy on the dependent service.
What is the difference between depends_on with and without condition?
Without condition, depends_on only waits for the container to start (service_started). With condition: service_healthy, it waits until the healthcheck passes. With condition: service_completed_successfully, it waits until the container exits with code 0.
Do I need a wait-for-it script with Docker Compose?
Not anymore. The modern approach is to use healthcheck with condition: service_healthy in docker-compose.yml (available since Compose v2.1). Wait-for-it scripts are a legacy workaround from before healthcheck conditions were supported.
What happens if the healthcheck never passes?
Docker Compose will wait indefinitely (or until the healthcheck exhausts its retries and marks the service as unhealthy). If the dependent service has condition: service_healthy, it will not start. Use docker compose ps to check the health status and docker inspect to view healthcheck logs.
Key Takeaways
depends_ononly controls start order, not service readiness.- Use
healthcheck+condition: service_healthyto wait until a service is actually ready. - Always add
start_periodto give services time to initialize before health checks count. - Add retry logic in your app code as a safety net — even with healthchecks.
- Use our Docker Compose Generator to create properly configured docker-compose.yml files.