Skip to main content

Docker Compose Healthcheck: Examples and depends_on (2026)

A Docker Compose healthcheck is a command that Docker runs inside a container at an interval. Exit code 0 means healthy, 1 means unhealthy. You define it with the healthcheck key on a service. Its main use in Compose is start order: with depends_on and condition: service_healthy, a service waits until its dependency is actually ready, not just started.

Last updated: October 2026.

A minimal example​

services:
db:
image: postgres:18
environment:
POSTGRES_PASSWORD: example
healthcheck:
test: ["CMD-SHELL", "pg_isready -h 127.0.0.1 -U postgres"]
interval: 5s
timeout: 5s
retries: 5
start_period: 10s

web:
image: ghcr.io/example/web:latest
depends_on:
db:
condition: service_healthy

docker compose up now starts db, waits for pg_isready to succeed, and only then starts web.


Healthcheck options​

OptionDefaultWhat it does
testfrom the imageThe command to run
interval30sTime between checks
timeout30sA check that runs longer counts as failed
retries3Failed checks in a row before the status is unhealthy
start_period0sStartup time. Failures here do not count toward retries
start_interval5sTime between checks during start_period. Needs Docker Engine 25.0+
disablefalsetrue turns off a healthcheck set in the image

The defaults are slow. With interval: 30s, a dependent service waits at least 30 seconds, even if the database was ready after 2. For start order, use a short interval (5s) or a start_period with start_interval: 1s.

test forms​

# Exec form: runs the binary directly, no shell
test: ["CMD", "redis-cli", "ping"]

# Shell form: runs in /bin/sh, so pipes, || and env vars work
test: ["CMD-SHELL", "curl -f http://localhost:8080/health || exit 1"]

# A plain string is the same as CMD-SHELL
test: curl -f http://localhost:8080/health || exit 1

# Turn off the image's healthcheck
test: ["NONE"]

To use a container env var in the check, write $$VAR, not $VAR. Compose replaces $VAR with a value from your host or .env before the container starts. $$ passes a literal $ to the shell inside the container.


Healthchecks for common services​

PostgreSQL​

healthcheck:
test: ["CMD-SHELL", "pg_isready -h 127.0.0.1 -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval: 5s
timeout: 5s
retries: 5

Use -h 127.0.0.1. On the first start, the image runs a temporary server for the init scripts. That server listens only on the Unix socket. Without -h, pg_isready can report "healthy" during init, and your app connects to a server that is about to restart. Full setup: PostgreSQL with Docker Compose.

MySQL / MariaDB​

healthcheck:
test: ["CMD-SHELL", "mysqladmin ping -h 127.0.0.1 -u root -p$${MYSQL_ROOT_PASSWORD}"]
interval: 5s
timeout: 5s
retries: 10
start_period: 30s

Same reason for -h 127.0.0.1: the MySQL image's temporary init server runs with --skip-networking, so a check over TCP fails until the real server is up. MySQL's first start is slow, so give it a start_period. For MariaDB, the official image ships a script: test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"].

Redis​

healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 5

With a password, use ["CMD-SHELL", "redis-cli -a $${REDIS_PASSWORD} ping | grep PONG"].

HTTP app​

healthcheck:
test: ["CMD-SHELL", "curl -fsS http://localhost:8080/health || exit 1"]
interval: 10s
timeout: 3s
retries: 3
start_period: 20s

-f makes curl fail on HTTP 4xx and 5xx. Without it, a 500 response still counts as healthy.

Many small images (alpine, -slim, distroless) have no curl. Options:

  • Alpine and BusyBox images have wget: test: ["CMD", "wget", "-q", "--spider", "http://localhost:8080/health"]
  • Use the runtime you already have, for example ["CMD", "node", "-e", "fetch('http://localhost:8080/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"]
  • Distroless images have no shell. Build a small healthcheck binary into the image, or check from outside the container.

The check runs inside the container, so localhost is the container itself, not your host. Use the port the app listens on inside the container, not the published port.


depends_on conditions​

services:
migrate:
image: ghcr.io/example/web:latest
command: ["./migrate", "up"]
depends_on:
db:
condition: service_healthy

web:
image: ghcr.io/example/web:latest
depends_on:
db:
condition: service_healthy
restart: true
migrate:
condition: service_completed_successfully
Conditionweb starts when
service_startedThe dependency container has started. Same as short depends_on: [db]
service_healthyThe dependency's healthcheck reports healthy
service_completed_successfullyThe dependency ran and exited with code 0. Use for migrations and one-off setup jobs

Two more options:

  • restart: true: Compose restarts web after it updates db, for example when up recreates db after a config change. Needs Compose 2.17.0+.
  • required: false: Compose only warns if the dependency is not started or not available. Needs Compose 2.20.0+.

service_healthy needs a healthcheck on the dependency, in the Compose file or in its image. Without one, docker compose up stops with an error instead of waiting.


Check the health status​

# STATUS column shows (healthy), (unhealthy) or (health: starting)
docker compose ps

# Last checks with their output and exit codes
docker inspect --format '{{json .State.Health}}' <container> | jq

# Start the stack and block until every service is running or healthy
docker compose up -d --wait

docker inspect keeps the output of the last checks (the first 4096 bytes of each). Read it first when a check fails, because it usually says why. docker compose up --wait is useful in CI and deploy scripts: it fails if a service does not become running or healthy.


An unhealthy container is not restarted​

This surprises most people. Plain Docker and Docker Compose only report unhealthy. They do not restart the container. restart: unless-stopped reacts when the process exits, not when a healthcheck fails. See Docker Compose restart policies.

If a service hangs but does not exit, it stays unhealthy until you restart it by hand. Ways around this:

  • Make the app exit on a fatal error, so the restart policy brings it back.
  • Run a watcher container such as willfarrell/autoheal, which restarts containers marked unhealthy. It needs access to the Docker socket, which gives it full control of the host.
  • Run on an orchestrator. Kubernetes uses liveness probes to restart hung containers, and readiness probes to keep traffic away from containers that are not ready. See Docker Compose vs Kubernetes.

Healthchecks on Hostim.dev​

On Hostim.dev, you set a health check path on an app with an HTTP port, for example /health. Hostim sends traffic to a new instance only after the path returns 200. During a redeploy, the old instance keeps serving until the new one is ready. See health checks.

When you paste a Compose file, Postgres, MySQL and Redis services become managed databases. They run as separate services, so you do not need depends_on for them. The healthcheck block of your app is not imported. Set the health check path in the app settings instead.

👉 Deploy a Compose stack with managed databases

Frequently asked questions

How do I make a service wait for another in Docker Compose?

Add a healthcheck to the dependency, then use depends_on with condition: service_healthy on the waiting service. A plain depends_on only waits for the container to start, not for the application inside it to be ready.

What are the default healthcheck timings in Docker Compose?

interval 30s, timeout 30s, retries 3, start_period 0s and start_interval 5s. These are the same defaults as the Dockerfile HEALTHCHECK instruction. For start order, use a shorter interval such as 5s, or dependent services wait at least 30 seconds.

Does Docker Compose restart unhealthy containers?

No. Docker and Docker Compose only mark the container as unhealthy. The restart policy reacts only when the process exits. To restart hung containers, make the app exit on fatal errors, use a watcher such as autoheal, or run on an orchestrator with liveness probes, such as Kubernetes.

Why does my Docker healthcheck with curl fail?

Common causes: the image has no curl (alpine, slim and distroless images), the URL uses the published host port instead of the port inside the container, or the app is still starting. Use wget or the app's own runtime instead of curl, check localhost on the internal port, and add a start_period.

How do I use environment variables in a Docker Compose healthcheck?

Use the CMD-SHELL form and write $$VAR instead of $VAR, for example pg_isready -U $${POSTGRES_USER}. With a single $, Compose replaces the variable from the host or .env file before the container starts. $$ passes it through to the shell inside the container.