CertGrid CertGrid
Concepts·Docker

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

Tested on the versions above. Startup timings vary by host and image. The exit statuses and states are what to match.

One Docker host is all this guide needs. Nothing here depends on a second machine, and the hardware above is modest on purpose - a 2 core, 4 GB VM runs everything in this path.
Server NameIP AddressOSRolesCPURAMHDD
DOCKER01192.168.0.21Ubuntu 26.04 LTSDocker Host2 Core4 GB50 GB

Before you start

  1. Up is not ready

    This is the failure the rest of the guide exists to prevent. Immediately after up -d reports the container Started, a request to its published port fails. Nothing is broken - the container exists and the process is booting. curl exit 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 peer

    Expected 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.

  2. 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_isready for PostgreSQL, an HTTP request for a web service, redis-cli ping for Redis.

    bash
    cat compose.yaml    healthcheck:      test: ["CMD-SHELL", "pg_isready -U postgres -d appdb"]      interval: 3s      timeout: 3s      retries: 10

    Expected 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 postgres would pass long before the server accepts connections - which would defeat the whole exercise.

  3. Gate the dependent service on it

    Bare depends_on only orders container creation; it does not wait for anything to work. The long form with condition: service_healthy makes 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: true

    Expected resultThe resolved configuration showing the condition.

    Success conditioncondition: service_healthy appears in the resolved output. If it does not, you wrote the list form.

  4. 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 Started

    Expected resultWaiting, then Healthy, then the dependent service starts.

    Success conditionThe dependent service starts after Healthy, not after Started.

  5. 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=1

    Expected 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. starting means the check has not passed yet and has not exhausted its retries.

  6. 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-stopped reacts 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 not

    Expected resultNothing, on a healthy machine.

    Success conditionYou know this filter exists. It is the cheapest possible health dashboard for a single host.

Troubleshooting

Official sources