Docker Compose Health Checks and Dependencies
Up does not mean ready, and the gap between them is where flaky startups live. Watch a container that is Up refuse a connection, then close the gap with a healthcheck and a depends_on condition that actually waits.
Docker Compose Guide 26 of 46 Intermediate
- OSUbuntu 26.04 LTS (resolute)
- Docker Engine29.7.2
- Docker Compose5.4.0
- Architectureamd64
- TimeAbout 13 min
- Reviewed21 August 2026
Tested on the versions above. Startup timings vary by host and image. The exit statuses and states are what to match.
| Server Name | IP Address | OS | Roles | CPU | RAM | HDD |
|---|---|---|---|---|---|---|
| DOCKER01 | 192.168.0.21 | Ubuntu 26.04 LTS | Docker Host | 2 Core | 4 GB | 50 GB |
Before you start
- A running multi-service project - guide 25 in this path.
- Reading container state with inspect - guide 7 in this path.
-
Up is not ready
This is the failure the rest of the guide exists to prevent. Immediately after
up -dreports the container Started, a request to its published port fails. Nothing is broken - the container exists and the process is booting.curlexit 56 is a connection reset, which is what you get when the port is bound but nothing is serving yet.bash Example session docker compose up -d --build Container cg-watch-web-1 Startedcurl -s http://localhost:8099# no output, exit status 56 - connection reset by peerExpected resultAn immediate request failing despite the container being Started.
Success conditionYou have reproduced the race. Any script that starts a stack and immediately tests it will fail intermittently on a loaded machine, and pass on yours.
-
Define what ready means
A healthcheck is a command Docker runs inside the container on a schedule. Exit 0 means healthy. The right check asks the question a caller would ask - can I actually query this - rather than merely whether a process is alive.
pg_isreadyfor PostgreSQL, an HTTP request for a web service,redis-cli pingfor Redis.bash cat compose.yaml healthcheck: test: ["CMD-SHELL", "pg_isready -U postgres -d appdb"] interval: 3s timeout: 3s retries: 10Expected resultA test command, plus how often to run it and how many failures to tolerate.
Success conditionThe test is a real query, not a process check.
pgrep postgreswould pass long before the server accepts connections - which would defeat the whole exercise. -
Gate the dependent service on it
Bare
depends_ononly orders container creation; it does not wait for anything to work. The long form withcondition: service_healthymakes Compose block until the healthcheck passes. The two look similar in a file and behave completely differently.bash # waits only for the container to be created - almost never what you want:depends_on: [db] # waits until the healthcheck passes:docker compose config | grep -A3 depends_on depends_on: db: condition: service_healthy required: trueExpected resultThe resolved configuration showing the condition.
Success condition
condition: service_healthyappears in the resolved output. If it does not, you wrote the list form. -
Watch the gate work
The Waiting and Healthy lines are Compose blocking on the healthcheck. Everything after them happened only because the check passed. This is the same transcript as the previous guide, read for a different reason - here the interesting part is the pause, not the result.
bash Example session docker compose up -d Container cg-stack-db-1 Started Container cg-stack-db-1 Waiting Container cg-stack-db-1 Healthy Container cg-stack-api-1 Starting Container cg-stack-api-1 StartedExpected resultWaiting, then Healthy, then the dependent service starts.
Success conditionThe dependent service starts after Healthy, not after Started.
-
Read the health state and its history
Docker keeps a log of recent probe results, which is the difference between guessing and knowing why a service is considered unhealthy. The log holds the command's own output, so a failing check tells you what it saw.
bash Example session docker inspect cg-stack-db-1 --format "health={{.State.Health.Status}} checks={{len .State.Health.Log}}"health=healthy checks=1Expected resultA health status of healthy and at least one recorded check.
Verify it worked
bash Example session docker compose ps --format "table {{.Service}}\t{{.Status}}"SERVICE STATUSdb Up 3 seconds (healthy)Success conditionYou can read both the state and how many probes have run.
startingmeans the check has not passed yet and has not exhausted its retries. -
Healthchecks are not only for startup
The same check runs for the life of the container, so a service that degrades later is marked unhealthy while still running. Docker does not restart it for you -
restart: unless-stoppedreacts to the process exiting, not to a failed healthcheck. Treat the health state as a signal for your orchestration or monitoring to act on.bash docker ps --filter health=unhealthy --format "{{.Names}} {{.Status}}"# empty when everything is healthy - worth alerting on when it is notExpected resultNothing, on a healthy machine.
Success conditionYou know this filter exists. It is the cheapest possible health dashboard for a single host.
Troubleshooting
The service never becomes healthy
Why: Usually the test command is not available inside the image.
curlin particular is absent from most slim and alpine images, so the check fails immediately and forever.Fix:Run the exact test command inside the container by hand first. Use a tool the image actually has -
wgeton alpine, or the database's own client.bash docker compose exec db pg_isready -U postgres -d appdb# if this fails by hand, the healthcheck cannot pass eitherdependency failed to start: container is unhealthy
Why: The dependency exhausted its retries, so Compose gave up and never started the dependent service.
Fix:Read the health log for the failing probe's output. Increase
retriesor addstart_periodfor a service that is legitimately slow to boot, rather than loosening the test itself.bash docker inspect NAME --format "{{json .State.Health}}"The stack works locally but fails in CI
Why: The classic symptom of relying on timing. A slower or busier machine widens the gap between Started and ready, and an unguarded dependency loses the race.
Fix:Add the healthcheck and the condition. Do not paper over it with
sleep- it makes fast machines slow and slow machines still flaky.bash # a fixed sleep is a guess, not a gatedocker compose up -d --wait# --wait blocks until services are healthy, and exits non-zero if they are notA service has no health annotation at all in ps
Why: No healthcheck is defined - neither in the Compose file nor in the image. Absent is not the same as unhealthy.
Fix:Add one in the Compose file, or a HEALTHCHECK instruction in the Dockerfile so every user of the image inherits it.
bash docker image inspect IMAGE --format "{{json .Config.Healthcheck}}"# null means the image defines none