Depends On in Docker Compose Does Not Mean What You Think It Means

by Arif Ikhsanudin, Backend Developer

The depends_on that doesn't actually help

You added depends_on: db to your application service in Docker Compose. You thought this meant "don't start the app until the database is ready." You still get connection errors on first startup. The database container starts before the app container, but the PostgreSQL server inside it isn't ready to accept connections when your application tries to connect.

The confusion is between container start order and service readiness. depends_on without conditions only controls the former.

What depends_on without conditions actually does

services:
  app:
    depends_on:
      - db

  db:
    image: postgres:16-alpine

This tells Compose: start db before starting app. The db container will be in running state when app starts. That's it. It says nothing about whether PostgreSQL inside the container is initialized, has created the data directory, or is accepting connections.

PostgreSQL's startup sequence:

  1. Container starts → Docker reports running
  2. postgres process initializes data directory (if first run)
  3. PostgreSQL applies configuration files
  4. PostgreSQL starts listening on port 5432
  5. PostgreSQL is ready to accept connections

Steps 2–5 take 5–30 seconds depending on hardware and whether it's a first-run initialization. Your application starts immediately after step 1.

depends_on with conditions: what actually works

Compose v2 (the current version) introduced condition to depends_on:

services:
  app:
    depends_on:
      db:
        condition: service_healthy

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: appuser
      POSTGRES_DB: myapp
      POSTGRES_PASSWORD: secret
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U appuser -d myapp"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 30s

Now depends_on: db: condition: service_healthy means: don't start app until db reports healthy. Compose polls the db health check and waits until it passes before starting app.

Three conditions are available:

  • service_started: The default when condition is omitted. Container is running. Same as the old bare depends_on: db behavior.
  • service_healthy: Container is running AND health check is passing. Requires a healthcheck directive on the dependency.
  • service_completed_successfully: Container has exited with code 0. For one-shot services like database migrations.

The health check is not optional with service_healthy

If you use condition: service_healthy without a healthcheck on the dependency, Compose will wait indefinitely because the dependency will never be considered healthy. Or in some Compose versions it throws an error immediately.

Always pair condition: service_healthy with an appropriate health check:

PostgreSQL:

healthcheck:
  test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-postgres}"]
  interval: 10s
  timeout: 5s
  retries: 5
  start_period: 30s

MySQL:

healthcheck:
  test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "--silent"]
  interval: 10s
  timeout: 5s
  retries: 5
  start_period: 30s

Redis:

healthcheck:
  test: ["CMD", "redis-cli", "ping"]
  interval: 5s
  timeout: 3s
  retries: 10

RabbitMQ:

healthcheck:
  test: ["CMD", "rabbitmq-diagnostics", "ping"]
  interval: 10s
  timeout: 5s
  retries: 5
  start_period: 30s

HTTP service:

healthcheck:
  test: ["CMD", "wget", "-qO-", "http://localhost:8080/health"]
  interval: 30s
  timeout: 10s
  retries: 3
  start_period: 60s

service_completed_successfully for migrations

A pattern that comes up in Spring Boot and other JVM apps: running database migrations as a separate step before the application starts.

services:
  db:
    image: postgres:16-alpine
    # ... healthcheck as above

  migrate:
    image: flyway/flyway:10-alpine
    command: >
      -url=jdbc:postgresql://db:5432/${DB_NAME}
      -user=${DB_USER}
      -password=${DB_PASSWORD}
      migrate
    volumes:
      - ./src/main/resources/db/migration:/flyway/sql:ro
    depends_on:
      db:
        condition: service_healthy

  app:
    build: .
    depends_on:
      db:
        condition: service_healthy
      migrate:
        condition: service_completed_successfully

The app waits for both the database to be healthy and the migration container to have exited with code 0. If migrations fail (exit code non-zero), Compose doesn't start the app, and the compose up command exits with an error. This is the correct behavior.

The startup_completed condition: not what you might need

There's a fourth condition worth knowing about: service_started is not the same as "the service inside the container has fully started." Even with health checks, there can be a brief period after the health check passes where the service is still warming up. For most use cases service_healthy is sufficient — the health check is designed to pass when the service is ready.

If your application has its own startup health check endpoint that returns 200 only when the application is fully initialized (connection pools established, caches warmed), use that in your health check. Don't use a basic TCP connectivity check as a proxy for application readiness if you can check application readiness directly.

What depends_on doesn't solve

depends_on only helps at initial startup. If a dependency becomes unavailable after the app has started — database restarts, Redis is OOMKilled, a downstream service crashes — depends_on does nothing. Your application needs to handle transient failures with retry logic and circuit breaking.

For database connections, use a connection pool with retry configuration:

# Spring Boot + HikariCP
SPRING_DATASOURCE_HIKARI_CONNECTION_TIMEOUT: 30000
SPRING_DATASOURCE_HIKARI_INITIALIZATION_FAIL_TIMEOUT: 60000

initialization-fail-timeout=-1 tells HikariCP to retry indefinitely during startup rather than failing immediately if the database isn't available. This provides an additional safety net beyond depends_on ordering — even if your Compose health check configuration isn't quite right, your connection pool will retry.

The version of Compose you're using matters

condition on depends_on is a Compose v2 feature. If you're using the old docker-compose (v1) binary — identifiable by the version: "3" or version: "2" at the top of your Compose file — the condition syntax may not work or may be silently ignored.

Check which version you're using:

docker compose version   # v2 — built into Docker
docker-compose version   # v1 — separate Python binary (deprecated)

If you're on v1, migrate to docker compose (v2). V1 reached end-of-life in 2023.

The version: key at the top of Compose files is also deprecated in Compose v2 and can be removed. It's ignored by current versions.

Scale Your Backend - Need an Experienced Backend Developer?

We provide backend engineers who join your team as contractors to help build, improve, and scale your backend systems.

We focus on clean backend design, clear documentation, and systems that remain reliable as products grow. Our goal is to strengthen your team and deliver backend systems that are easy to operate and maintain.

We work from our own development environments and support teams across US, EU, and APAC timezones. Our workflow emphasizes documentation and asynchronous collaboration to keep development efficient and focused.

  • Production Backend Experience. Experience building and maintaining backend systems, APIs, and databases used in production.
  • Scalable Architecture. Design backend systems that stay reliable as your product and traffic grow.
  • Contractor Friendly. Flexible engagement for short projects, long-term support, or extra help during releases.
  • Focus on Backend Reliability. Improve API performance, database stability, and overall backend reliability.
  • Documentation-Driven Development. Development guided by clear documentation so teams stay aligned and work efficiently.
  • Domain-Driven Design. Design backend systems around real business processes and product needs.

Tell us about your project

Our offices

  • Copenhagen
    1 Carlsberg Gate
    1260, København, Denmark
  • Magelang
    12 Jalan Bligo
    56485, Magelang, Indonesia

More articles

How to Write a Statement of Work That Protects Both Sides

A good statement of work does not just protect the contractor. It gives the client clarity, reduces their anxiety, and prevents the misunderstandings that destroy otherwise good engagements.

Read more

How to Build Trust With a Client You Have Never Met in Person

Remote work removes the in-person trust-building signals that most professional relationships rely on. The contractors who succeed at distance learn to replace those signals with something better.

Read more

Git Hooks: Automate the Checks Your Team Keeps Forgetting

Git hooks run scripts at specific points in the Git workflow — before a commit, before a push, after a merge. They are the lightweight automation layer that enforces standards locally before code ever reaches CI.

Read more

REST Is Not Just Using HTTP. Here Is What It Actually Means.

Most APIs labeled “REST” ignore the constraints that actually define it. Understanding what REST really requires leads to more scalable, evolvable systems—but also reveals when not to use it.

Read more