Docker Compose

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.

Quick Fix
  1. 1. Add a healthcheck to the service your app depends on
  2. 2. Set condition: service_healthy in your depends_on
  3. 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:

Container Started
The Docker container process has been launched. The entrypoint is running. This is what depends_on waits for by default.
Service Ready
The application inside the container has finished initializing and is accepting connections. This takes seconds or even minutes after the container starts.
# 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:6379

How healthcheck parameters work:

ParameterMeaningDefault
testCommand to check if the service is healthy
intervalTime between health checks30s
timeoutMax time for a single check to complete30s
retriesConsecutive failures before marking unhealthy3
start_periodGrace period before health checks count0s

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 conn

Solution 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:
      - postgres

Why 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

ServiceHealthcheck 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

Missing healthcheck on the dependency service
condition: service_healthy has no effect if the dependency service does not define a healthcheck. The service will start immediately and the condition is silently ignored.
Using the wrong condition value
The valid conditions are service_started (default), service_healthy, and service_completed_successfully. Using "service_ready" or "ready" will cause a validation error.
Missing start_period in healthcheck
Without start_period, the healthcheck starts counting retries immediately. If the service takes 10 seconds to initialize and retries is 3 with interval 5s, it may be marked unhealthy before it even finishes starting.
Using localhost instead of the service name
In Docker Compose, services communicate using the service name as the hostname. Use postgres:5432, not localhost:5432. localhost inside a container refers to the container itself.
Setting depends_on on the wrong service
depends_on goes on the service that needs to wait, not the service being waited for. If web needs postgres, put depends_on under web, not under postgres.
Forgetting that depends_on is not required for networking
Services on the same Docker network can communicate regardless of depends_on. depends_on only controls startup order and health conditions, not network access.

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_on only controls start order, not service readiness.
  • Use healthcheck + condition: service_healthy to wait until a service is actually ready.
  • Always add start_period to 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.

Related Resources