Docker Compose healthcheck depends_on condition service_healthy not working: the 2026 fix guide
Docker Compose healthcheck depends_on condition service_healthy not working: the 2026 fix guide
Your app container starts, your database container starts, and your app crashes with connection refused before Postgres finishes initializing. You added depends_on with condition: service_healthy and it still happens. Here is the working config first:
services:
db:
image: postgres:16-alpine
environment:
POSTGRES_PASSWORD: devpass
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres -d postgres"]
interval: 5s
timeout: 5s
retries: 10
start_period: 30s
api:
build: .
depends_on:
db:
condition: service_healthy
Now here is what it actually does, and the six reasons it silently does nothing.
Why does depends_on with condition service_healthy appear to be ignored?
It is ignored most often because the Compose file version header is still present, or because the healthcheck itself never reports healthy — Compose waits forever, then the dependency never resolves and the start order looks broken.
The version: key is the single biggest cause. Compose Spec (the format used by Docker Compose v2, which is what ships with Docker Desktop and the docker compose plugin in 2026) deprecated version: in 2023. If you keep a legacy version: "3.8" line alongside a spec-style file, Compose can fall back to older parsing behaviour and the long-form depends_on map gets dropped or misinterpreted.
The fix is one line: delete the version: key entirely. Compose v2 warns about it and the long-form condition syntax is only guaranteed under the Compose Specification.
# WRONG - legacy header can change parsing behaviour
version: "3.8"
services:
...
# RIGHT - Compose Spec, no version key
services:
...
Verify which Compose you are actually running before you debug anything else:
docker compose version
# Docker Compose version v2.29.x → Compose Spec, long-form depends_on supported
If that prints docker-compose version 1.29.x (the old Python binary with a hyphen), you are on a tool that predates condition: service_healthy support in its modern form. Install the Compose v2 plugin.
Is your healthcheck actually reporting healthy, or just running?
A healthcheck that exits non-zero forever keeps the container in starting, and a dependent service with condition: service_healthy will never start — it waits indefinitely rather than failing fast.
This is the failure mode that costs people hours: the api container never appears in docker compose ps at all, so it looks like Compose ignored the dependency. It did not. It is holding the start until the healthcheck passes, and the healthcheck is failing.
Check the actual health state, not the running state:
docker compose ps
# NAME STATUS
# db Up 2 minutes (health: starting)
# api Created <-- waiting, not broken
docker inspect --format '{{json .State.Health}}' $(docker compose ps -q db) | jq
The Health.Log array in that output contains the last few check results, including the exit code and stdout/stderr of the test command. Read it. It tells you exactly why the probe fails.
The four healthcheck fields that break the probe
testuses shell syntax withoutCMD-SHELL.["CMD", "curl -f http://localhost:8080/health"]tries to exec a binary literally namedcurl -f .... Use["CMD-SHELL", "curl -f http://localhost:8080/health"]or split the exec form into separate array elements.- The probe binary does not exist in the image.
curlandwgetare absent from most slim and distroless images.postgres:16-alpinehaspg_isready;alpinehaswget;node:22-slimhas neithercurlnorpg_isready. start_periodis missing or too short. Without it, failed checks during startup count towardretriesand the container can be marked unhealthy before the service is even up.- The probe checks
localhostinside a container that binds elsewhere. If the app listens on0.0.0.0:8080,localhostworks. If it binds only to a specific interface, it does not.
What is the difference between service_started, service_healthy, and service_completed_successfully?
service_started only waits for the container process to launch, service_healthy waits for the healthcheck to pass, and service_completed_successfully waits for a one-shot container to exit with code 0.
This distinction is the whole reason the feature exists. service_started is what plain depends_on: [db] gives you, and it is why the original problem happens: the Postgres process has started but is not accepting connections yet.
depends_on:
db:
condition: service_started # process launched, nothing more
migrate:
condition: service_completed_successfully # exit code 0 required
cache:
condition: service_healthy # healthcheck must pass
service_completed_successfully is the correct condition for migration and seed containers. Pair it with restart: "no" on the one-shot service so Compose does not restart it and deadlock the chain.
Why does it work with docker compose up but fail in Docker Swarm or Kubernetes?
condition: service_healthy is a Compose-only feature and has no equivalent in Swarm’s deploy stanza or in Kubernetes manifests.
If you run docker stack deploy against the same file, the long-form depends_on is ignored — Swarm does not implement it. Compose Spec documents depends_on conditions as applying to docker compose up and docker compose run only. The Docker documentation on startup order (2026) states plainly that Compose controls start order, not readiness of the application inside the container, and that service_healthy is the mechanism for the latter.
In Kubernetes the equivalent is a readinessProbe plus an initContainer that polls the dependency. There is no depends_on. Do not try to port the Compose file directly.
Does depends_on with service_healthy guarantee my app will connect successfully?
No. service_healthy guarantees the healthcheck passed at the moment it was evaluated, and nothing about the state one second later.
This is the trap that catches people after they fix the parsing bug. A database can pass pg_isready and still reject a connection if the target database has not been created, if max connections are exhausted, or if a replica is mid-promotion. Compose has no retry semantics for the dependent service.
The durable fix is application-side retry with backoff, so the container survives a dependency that flaps:
# entrypoint.sh - wait for a TCP port, then exec the real process
until pg_isready -h db -U postgres -d postgres; do
echo "waiting for db..."
sleep 2
done
exec "$@"
Treat depends_on as start ordering and the retry loop as correctness. They solve different problems, and production needs both.
How do you debug a dependency chain that never resolves?
Run docker compose up --wait — it blocks until every service is healthy and exits non-zero with the failing service named, instead of hanging silently.
docker compose up --wait --wait-timeout 120
# exits 1 with the unhealthy service listed if a healthcheck never passes
--wait and --wait-timeout are the fastest way to turn a silent hang into a diagnosable failure. Combine with docker compose logs db for the container’s own output and the Health.Log inspect from earlier for the probe’s output.
Order of operations when it still will not work:
docker compose config— prints the fully resolved file. If your long-formdepends_onis missing from this output, the parser dropped it and theversion:key is the likely cause.docker compose ps— is the dependencystartingorunhealthy?docker inspectthe health log — what is the probe’s exit code?- Exec into the dependency and run the probe command by hand.
Step 1 catches more cases than the other three combined. docker compose config is the ground truth for what Compose actually parsed, and it is the first command to run when the file and the behaviour disagree.
The bottom line
Delete the version: key, confirm you are on Compose v2, and make sure your healthcheck command exists inside the image and exits 0 when the service is genuinely ready. Then add application-side retry, because service_healthy is a start-order gate, not a connection guarantee.
The failure mode to watch for: a healthcheck that passes on the wrong signal. A TCP port check on a database that is still running recovery will report healthy while every query fails. Probe the thing you actually depend on — pg_isready for Postgres, a real HTTP 200 for a web service — not just the port being open.