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:

FieldDefaultDescription
test(required)The health-checking command
interval30sInterval between tests
timeout30sPer-test timeout
retries3Failures before considered unhealthy
start_period0sInitial 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 /health or /ready endpoint. 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:

PatternUse 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 containerData setup needed before the main service
No depends_onIndependent 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:

  • nginx depends on web
  • web depends on api
  • api depends on db, cache, mq
  • worker depends on api, mq

All services have healthchecks. Container restart policy is unless-stopped except for those needing graceful termination.

Startup order (parallelism maximized):

  1. db, cache, mq start in parallel
  2. Once all three are healthy, api and worker start in parallel
  3. Once api is healthy, web starts
  4. Once web is healthy, nginx starts

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_on manages 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_healthy in depends_on.
  • Healthchecks must genuinely test whether the service can accept requests, not just whether the process is running.
  • Startup orderruntime dependency. Runtime needs retry logic in the application.
  • Parallel startup — services without depends_on on 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.

← Previous: Volume & Network   Next: Healthcheck →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact