Docker Compose healthcheck depends_on Condition Not Working: How to Fix Service Startup Order in 2026

Docker Compose healthcheck depends_on Condition Not Working: How to Fix Service Startup Order in 2026

The fix is one line: depends_on with a bare list only waits for the container to start, not to be ready, so you need the long-form condition: service_healthy block plus a real healthcheck on the dependency. The one-liner that actually gates startup:

services:
  api:
    depends_on:
      db:
        condition: service_healthy
  db:
    image: postgres:16
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      timeout: 3s
      retries: 10

Now here is what it actually does, and why the short form silently fails.

Why depends_on alone does not wait for readiness

Short-form depends_on only controls container creation order, not application readiness. Compose documents this explicitly: the short syntax “does not wait for the dependency to be ready” — it starts the dependent service as soon as the dependency’s container has been created. On a Postgres image, the container is “running” several seconds before the server accepts connections, which is the exact window your app crashes in.

This is the failure mode that costs people hours: you see api exit with connection refused on db:5432, you add depends_on: [db], and it changes nothing because the ordering was never the problem — readiness was.

The long syntax is the only form that respects a health condition

Only the map form of depends_on accepts condition, and service_healthy is the one you want. The three valid conditions are service_started (default), service_healthy, and service_completed_successfully (for one-shot jobs like migrations).

services:
  migrate:
    image: myapp:2026.10
    command: ["./migrate", "up"]
    depends_on:
      db:
        condition: service_healthy

  api:
    image: myapp:2026.10
    depends_on:
      migrate:
        condition: service_completed_successfully
      db:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "curl", "-fsS", "http://localhost:8080/healthz"]
      interval: 10s
      timeout: 3s
      retries: 5
      start_period: 20s

Note start_period: it tells Docker to ignore failures during warmup so a slow boot does not burn your retry budget. Without it, a 25-second app boot with interval: 10s, retries: 5 can mark a perfectly healthy service as unhealthy.

A healthcheck that passes too early is worse than none

A healthcheck must test the thing your dependent service actually needs, not just that a process exists. pg_isready checks the socket accepts connections; it does not guarantee your schema is migrated. For an HTTP service, hit an endpoint that touches its real dependencies.

  • Postgres: pg_isready -U $POSTGRES_USER -d $POSTGRES_DB
  • MySQL: mysqladmin ping -h 127.0.0.1 -u root -p$MYSQL_ROOT_PASSWORD
  • Redis: redis-cli ping | grep PONG
  • Generic HTTP: curl -fsS http://localhost:PORT/healthz

Prefer CMD-SHELL when you need pipes or environment expansion, and CMD (exec form) when you do not — exec form avoids the shell and is easier to reason about.

Compose V2 versus the old version: key is a common source of “it does not work”

The top-level version: field is obsolete in Compose V2 (shipped with Docker Engine 20.10+ and standard in 2026) and is ignored with a warning. If you copied a 2019-era compose file, the version: "3.7" line is dead weight — remove it. The condition map form has been supported since Compose file format 2.1 and is fully supported by the V2 CLI, so a modern docker compose up (note: no hyphen) honors it. The legacy docker-compose V1 binary is end-of-life; running it is one way the condition appears to be ignored.

Check your version before debugging anything else:

docker compose version
# Docker Compose version v2.29.0 or later in 2026

Health status is visible only when you ask for it

docker compose ps shows a STATUS column that includes (healthy) or (unhealthy) once a healthcheck is defined. If the column just says Up, your healthcheck is not being applied — usually because it is nested under the wrong service or indented incorrectly. YAML indentation errors here fail silently more often than they throw.

docker compose ps
docker inspect --format '{{.State.Health.Status}}' $(docker compose ps -q db)

The second command prints starting, healthy, or unhealthy for a single container — the fastest way to confirm the healthcheck is running at all.

When the dependency never becomes healthy, the dependent never starts

This is the trap that replaces the old race condition with a hang. If db‘s healthcheck is misconfigured (wrong user, wrong port, missing password env), it stays unhealthy forever and api waits forever — docker compose up just sits there. Watch the logs of the dependency, not the dependent:

docker compose logs -f db
docker compose events --json

If you need a bounded wait, set retries on the healthcheck so an unhealthy service is reported rather than blocking indefinitely, and consider a restart policy on the dependent service.

Restart policies and healthchecks solve different problems

A restart: on-failure policy is not a substitute for service_healthy — it papers over the ordering problem by retrying crashes. Use both deliberately: the health condition prevents the first crash; the restart policy handles transient failures after startup. Relying on restarts alone means every cold start logs a burst of connection errors that mask real failures.

What to do next

Convert every depends_on that points at a stateful service (database, cache, message broker) to the map form with condition: service_healthy, give that service a healthcheck that tests the real interface, and add start_period to any slow-booting service. Then run docker compose up from a clean volume (docker compose down -v first) to confirm the ordering holds from zero — the failure mode only shows up on a cold start, which is exactly when you will not be watching.

The failure mode to watch for: a healthcheck that returns success before the service is truly ready (a port that is open but a schema that is not migrated) turns your startup race into a data-corruption bug that appears weeks later. Test the dependency, not the process.

Related Articles

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *