Skip to content
Blog

Healthchecks and depends_on: Making Docker Compose Startup Order Reliable

docker compose up racing your app? Use healthchecks and depends_on conditions like service_healthy to guarantee your services start in the right order — and stay that way.

Published on • October 28, 2026

AI Assistant

The classic failure: docker compose up, your API container starts, immediately tries to connect to Postgres, and dies — because Compose started it at the same time as the database, and Postgres takes eight seconds to accept connections while your app gives up in two. Retrying in application code helps, but “make everything retry everything” is how you end up with flaky systems that mask real failures. The Compose-native fix is a healthcheck wired into depends_on conditions — and getting it right is more subtle than the two-line snippet suggests.

The default: start order, not readiness

Plain depends_on: [db] guarantees only that db started before your API — that is, its container process was launched. It says nothing about whether Postgres finished initialization, finished restoring a dump, or is listening on the port. Docker has no built-in concept of “the service inside the container is ready,” which is why the start-order guarantee alone doesn’t prevent the race.

Step 1: teach Compose what “ready” means

A healthcheck is a command Compose (via the Engine) runs periodically inside the container; its exit code determines health:

services:
  db:
    image: postgres:16
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U appuser -d appdb"]
      interval: 5s
      timeout: 3s
      retries: 10
      start_period: 10s

The command is the part people get wrong. A port being open isn’t readiness — pg_isready, mysqladmin ping, redis-cli ping, and curl -f http://localhost:8080/healthz check that the application is actually serving. Use CMD-SHELL form when you need shell features (pipelines, environment variables); the CMD exec form avoids an extra shell process. start_period gives slow-initializing services a grace window before failures count against retries.

Step 2: gate on the condition

services:
  api:
    image: my-api
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_started

condition: service_healthy holds the API until the database’s healthcheck passes — not just starts. You can mix conditions: healthy-gating the database while merely start-gating Redis is often exactly right. There’s also service_completed_successfully for one-shot jobs — perfect for a migration container that must finish before the app begins:

  migrate:
    image: my-api
    command: ["./migrate"]
    depends_on:
      db:
        condition: service_healthy

  api:
    image: my-api
    depends_on:
      migrate:
        condition: service_completed_successfully

The traps

Healthy ≠ running forever. depends_on conditions apply at startup. Once the API is up, a later database outage won’t restart it — you still need application-level connection retry logic for steady-state resilience. Healthchecks tell Compose when to let dependents start; they don’t orchestrate runtime recovery.

One healthcheck, many consumers. With docker compose up --wait, Compose blocks until healthchecked services report healthy — useful in CI, where you want the pipeline to pause until the stack is genuinely ready before running tests.

Beware the healthcheck that lies. A check like CMD-SHELL exit 0 (or test -f /tmp/started) marks the container healthy by definition and quietly reintroduces the race you were fixing. Test the service, not the container.

Migration-then-start has a replica gotcha. If you scale the API (--scale api=3) alongside a service_completed_successfully migration, Compose’s handling depends on version — validate your exact Compose version’s behavior if you combine the two.

Debugging

docker compose ps shows a STATUS column with health (healthy, starting (5s), unhealthy (3)); docker inspect --format '{{json .State.Health}}' <container> gives the full history — every probe, its exit code, and output — which is exactly what you need when a service sits in starting forever. The usual culprits: a healthcheck command that doesn’t exist in a distroless/scratch image, checking the wrong port, or a start_period shorter than real initialization.

The bottom line

Compose gives you a two-part readiness protocol: healthchecks define what ready means for each service, and depends_on conditions (service_healthy, service_completed_successfully) enforce it as a startup gate. It converts “our deploy works if you wait and retry” into deterministic startup — while reminding you, honestly, that startup order and runtime resilience are two different problems, and a production stack needs both.