cloud-devops

Docker Compose depends_on Not Waiting? Fix It (2026)

July 27, 2026

Docker Compose depends_on Not Waiting? Fix It (2026)

Plain depends_on waits only until the dependency's container is running, not until the service inside is ready — so your app races the database and crashes. The fix: the long syntax with condition: service_healthy plus a real healthcheck on the dependency.12

TL;DR: depends_on: [db] starts db before web but does not wait for Postgres to accept queries. Switch to the long form (depends_on: { db: { condition: service_healthy } }) and give db a healthcheck such as pg_isready. Set start_period generously so a slow-booting database isn't marked unhealthy too early. If you see dependency failed to start: container ... is unhealthy, the dependency's healthcheck failed or is missing. depends_on is honored by docker compose up but ignored by Swarm's docker stack deploy.

What you'll learn

  • Why depends_on doesn't wait for your service to be ready
  • The difference between service_started, service_healthy, and service_completed_successfully
  • How to add a healthcheck and gate startup on it (with a working example)
  • Why a container gets stuck in health: starting and never proceeds
  • What dependency failed to start: container is unhealthy means and how to fix it
  • Which interval, timeout, retries, start_period, and start_interval values to use
  • Whether depends_on works with docker compose run and docker stack deploy
  • When to reach for wait-for-it.sh or app-level retries instead

Why isn't docker compose depends_on waiting for my service?

Because "started" is not "ready." The official Docker documentation is explicit: "On startup, Compose does not wait until a container is 'ready', only until it's running."2 With the short syntax, Compose creates the dependency first and then immediately starts the dependent — it does not wait for the dependency to become healthy.1

That distinction matters most for stateful services. A Postgres container reports "running" within milliseconds, but the database process needs additional time before it accepts SQL. Your app connects into that gap and dies. This short form is the trap:

services:
  web:
    build: .
    depends_on:
      - db     # only waits for the container to START, not to be READY
  db:
    image: postgres:18

Compose guarantees db is created before web, and that's all. As the docs put it, "With short syntax, Compose does not wait for dependency services to be 'healthy' before starting a dependent service."1 To wait for readiness you need the long syntax plus a healthcheck.

What's the difference between service_started, service_healthy, and service_completed_successfully?

They are the three condition values of the long depends_on syntax, and they define what "satisfied" means for a dependency:12

  • service_started — the dependency's container is running. This is identical to the short syntax: order only, no readiness.
  • service_healthy — the dependency's healthcheck has reported healthy. Use this for databases, brokers, and caches that need time to warm up.
  • service_completed_successfully — the dependency ran to completion and exited 0. Use this for one-shot init/migration jobs that must finish before the app starts.
ConditionWaits forNeeds a healthcheck?Typical use
service_startedContainer runningNoLoosely coupled services
service_healthyHealthcheck passesYesDatabases, brokers, caches
service_completed_successfullyExit code 0NoMigrations, seeders, init jobs

Only service_healthy waits for actual readiness, and it works only if the dependency defines a healthcheck. Without one, there is no health state to satisfy — which is the single most common reason startup gating "does nothing."1

How do I make depends_on wait until a service is healthy?

Two steps: add a healthcheck to the dependency, then reference it with condition: service_healthy. This is Docker's own canonical example, with web waiting on a healthy db and a started redis:2

services:
  web:
    build: .
    depends_on:
      db:
        condition: service_healthy
        restart: true
      redis:
        condition: service_started
  redis:
    image: redis
  db:
    image: postgres:18
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
      interval: 10s
      retries: 5
      start_period: 30s
      timeout: 10s

Now Compose waits for the db healthcheck to pass before it creates web.2 The pg_isready probe returns success only when Postgres is genuinely accepting connections, so the race disappears. The $$ escapes the variable so Compose passes $POSTGRES_USER through to the shell inside the container rather than interpolating it itself.

The optional restart: true sub-option (introduced in Docker Compose 2.17.0) means that if db is restarted by an explicit Compose operation such as docker compose restart, web is restarted too, so it re-establishes its connections.1 A related sub-option, required: false (Docker Compose 2.20.0), downgrades a missing dependency from an error to a warning.1 All of this is current as of Docker Compose v5.3.1, released July 7, 2026.3

Why does my container stay stuck in "health: starting"?

A container sits in health: starting until its first healthcheck passes (or until start_period ends and failures begin to count). If a dependent never starts and the dependency is stuck "starting," the usual causes are:

  • The dependency has no healthcheck at all, but something references it with condition: service_healthy. With no probe, it can never reach healthy, so the gate waits — or fails — forever.1
  • The healthcheck command is wrong — the binary isn't in the image, or the test never returns exit 0.
  • start_period is too short, so a database that needs 40 seconds to initialize is marked unhealthy before it finishes.

Docker's health states are starting, healthy, and unhealthy, driven by the probe's exit code: 0 means healthy, 1 means unhealthy. Failures that happen during start_period don't count toward the retry limit, and a success during that window ends the grace period early. Check the current state with docker inspect --format '{{.State.Health.Status}}' <container> or watch it live in docker compose ps.

What does "dependency failed to start: container is unhealthy" mean?

It means a condition: service_healthy gate failed: the dependency's healthcheck exhausted its retries without ever passing (or the dependency has no healthcheck to satisfy the condition).4 Modern Compose fails fast here rather than hanging indefinitely. To fix it, work through the dependency itself:

  1. Confirm the dependency actually has a healthcheck. If it's referenced with service_healthy but defines no probe, add one.
  2. Run the probe by hand inside the container: docker compose exec db pg_isready -U postgres. If it fails there, your test command is wrong.
  3. Raise start_period for slow starters. A cold Postgres or a JVM service often needs 60s or more before its first successful probe.
  4. Loosen retries/interval if the service is healthy but flaps under load during boot.

The error is a symptom of the dependency's health, not the dependent's — always debug the service named in the message.

What interval, retries, and start_period should I use?

Start from Docker Engine's HEALTHCHECK defaults and tune from there. The defaults are interval 30s, timeout 30s, retries 3, and start_period 0s.5 A default start_period of zero is why databases get flagged unhealthy on cold boots — always set it.

Sensible starting points by service type:

  • Databases (Postgres, MySQL): interval: 10s, timeout: 5s, retries: 5, start_period: 30s–60s.
  • HTTP APIs: a curl -f http://localhost:PORT/health test, interval: 15s, timeout: 5s, retries: 3, start_period: 20s.
  • Heavy JVM / cold-cache services: push start_period to 60s–120s.

There's also start_interval, which sets how often the probe runs during start_period — so you can poll every second at boot and back off to the slower interval once healthy. It requires Docker Compose 2.20.2+ and Docker Engine 25.0+:6

    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 30s        # steady-state cadence
      timeout: 5s
      retries: 5
      start_period: 60s    # grace window; failures here don't count
      start_interval: 1s   # poll every 1s during the grace window

Prefer the exec form ["CMD", "bin", "arg"] when you don't need a shell, and ["CMD-SHELL", "..."] when you need pipes or environment variables. Use test: ["NONE"] to disable a healthcheck an image inherited from its base.5

Does depends_on work with docker compose run and docker stack deploy?

Not everywhere. depends_on is a Compose-orchestration feature, and two contexts don't honor it the way docker compose up does:

  • docker stack deploy (Swarm mode) ignores depends_on entirely — with no warning. Swarm has no concept of ordered startup, so declared dependencies are silently dropped.7 In Swarm you must make services resilient to missing dependencies instead.
  • docker compose run <service> starts the depends_on services but does not wait for their healthchecks to report healthy, and --no-deps skips starting them at all.8

depends_on also only orders services within a single Compose project — it can't sequence containers you launch with separate docker run commands or in another project.

depends_on vs wait-for-it.sh vs app-level retries

All three solve "wait for readiness," at different layers:

  • depends_on + service_healthy is the built-in, declarative option. It's the right default because the readiness check lives with the dependency and every dependent inherits it.2
  • Wrapper scripts like wait-for-it.sh, wait-for, or dockerize block on an open TCP port before exec'ing your app. They're simple, but port-open is weaker than a real healthcheck: Postgres accepts TCP connections before it accepts queries, so a port probe can pass too early.
  • Application-level retries are the most robust layer. Docker's own long-standing guidance is to make the app resilient — reconnect with backoff — because startup order alone can't guarantee readiness across mid-run restarts. A circuit breaker or retry wrapper belongs in production regardless of your Compose config.

Best practice in 2026: use service_healthy for clean local and CI startup, and still add retry logic in the app so a database failover at 3 a.m. doesn't take the service down.

Bottom line

depends_on on its own only orders container starts — it never waits for readiness. Move to the long syntax, gate on condition: service_healthy, give every stateful dependency a real healthcheck with a generous start_period, and keep retry logic in the app for the cases Compose can't cover. If you're wiring this into a production stack, pair it with a hardened Docker Compose HTTPS setup and treat healthchecks the same way you'd treat Kubernetes readiness probes in a zero-downtime rollout. For database dependencies specifically, a resilient connection-pooling layer matters as much as startup order.

Footnotes

  1. Docker Docs — "Define services in Compose," depends_on (short and long syntax; restart introduced in 2.17.0, required in 2.20.0). https://docs.docker.com/reference/compose-file/services/#depends_on 2 3 4 5 6 7 8 9 10

  2. Docker Docs — "Control startup and shutdown order in Compose" (Compose does not wait until a container is "ready"; service_started / service_healthy / service_completed_successfully; canonical example). https://docs.docker.com/compose/how-tos/startup-order/ 2 3 4 5 6 7

  3. docker/compose GitHub Releases — v5.3.1, released July 7, 2026 (current stable). https://github.com/docker/compose/releases/tag/v5.3.1

  4. docker/compose issue #11474 and community reports — "dependency failed to start: container ... is unhealthy" occurs when a service_healthy dependency's healthcheck fails its retries or is missing. https://github.com/docker/compose/issues/11474 2

  5. Docker Docs — Dockerfile HEALTHCHECK reference (defaults: interval 30s, timeout 30s, retries 3, start-period 0s; exit 0 = healthy, 1 = unhealthy; CMD vs CMD-SHELL vs NONE). https://docs.docker.com/reference/dockerfile/#healthcheck 2 3 4

  6. docker/compose issue #10830 — start_interval requires Docker Compose 2.20.2+ and Docker Engine 25.0+. https://github.com/docker/compose/issues/10830

  7. moby/moby issue #40364 and docker/compose #13819 — depends_on is ignored by docker stack deploy in Swarm mode. https://github.com/moby/moby/issues/40364 2

  8. docker/compose issue #7681 — docker compose run starts dependencies but does not wait for their healthchecks; --no-deps skips them. https://github.com/docker/compose/issues/7681

Frequently Asked Questions

The short syntax ( depends_on: [db] ) only waits for the container to start running, not for the service to accept connections. 1 Use the long syntax with condition: service_healthy plus a healthcheck on the database.