Skip to main content

Compose Health Check

A configuration in a Docker Compose service that defines a command Docker runs periodically to determine if a container is healthy. Other services use depends_on with condition: service_healthy to wait until a dependency passes its health check before starting.

Compose Health Check — Waiting for Dependencies to Be Ready

What Is a Compose Health Check in Simple Terms?

Docker starts containers quickly — but services inside containers take time to be ready. PostgreSQL takes 3-5 seconds to accept connections after its container starts. Without health checks, your API container starts immediately, tries to connect to a PostgreSQL that is not ready yet, crashes, and goes into a restart loop.

A health check tells Docker how to verify a service is actually ready — not just started.

◈ DIAGRAM
WITHOUT health check:
postgres container starts
api container starts immediately (depends_on: postgres)
api tries DB connection -> connection refused
api crashes -> restart loop
WITH health check:
postgres container starts
postgres health check: pg_isready runs every 5s
postgres becomes healthy (pg_isready returns 0)
THEN api starts
api connects successfully

Health Check Configuration

YAML
services:
postgres:
image: postgres:15-alpine
environment:
POSTGRES_PASSWORD: secret
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s # run every 5 seconds
timeout: 3s # must complete within 3 seconds
retries: 5 # mark unhealthy after 5 consecutive failures
start_period: 10s # wait 10s before first check (startup grace period)
api:
depends_on:
postgres:
condition: service_healthy # wait until postgres is healthy

Health Check Commands for Common Services

YAML
# PostgreSQL
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres -d mydb"]
# MySQL
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
# Redis
healthcheck:
test: ["CMD", "redis-cli", "ping"]
# HTTP endpoint (requires curl in image)
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
# HTTP endpoint (wget — available in Alpine)
healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1",
"--spider", "http://localhost:8080/health"]
# RabbitMQ
healthcheck:
test: ["CMD", "rabbitmq-diagnostics", "ping"]
start_period: 30s # RabbitMQ takes longer to start
# MongoDB
healthcheck:
test: ["CMD", "mongosh", "--eval", "db.adminCommand('ping')"]

Container Health States

Bash
# Check health status
docker compose ps
# NAME STATUS
# postgres Up 2m (healthy) <- passing health check
# api Up 1m (healthy)
# redis Up 2m (unhealthy) <- failing health check!
# See health check output and failure reason
docker inspect redis-container \
--format '{{json .State.Health}}' | jq
# {
# "Status": "unhealthy",
# "FailingStreak": 3,
# "Log": [{
# "ExitCode": 1,
# "Output": "Could not connect to Redis at 127.0.0.1:6379"
# }]
# }
Tip

Always set start_period for services that take time to initialise. Without it, health check failures during the normal startup window count toward the failure threshold and can mark the service as unhealthy before it even finished starting. Use start_period: 10s for databases and start_period: 30s for services like RabbitMQ or Elasticsearch.

Common Mistake

Using depends_on: postgres without condition: service_healthy. Basic depends_on only waits for the container to start — not for PostgreSQL to accept connections. Your API will start and immediately try to connect to a database that needs 3-5 more seconds. Always add the health check condition.

Frequently Asked Questions

Why isn't a container's 'running' status enough to know it's ready?

A container can be in Running state while its application inside is still initializing, crashed into a hung loop, or listening but returning errors — Docker's process-level status doesn't inspect application health. A Compose health check runs an actual probe command (like curl against a health endpoint) on an interval, and only then reports the container as healthy, giving dependent services something meaningful to wait on instead of guessing based on a fixed sleep delay.

What's a common mistake with Compose health checks and depends_on?

Using plain `depends_on: [service]` without `condition: service_healthy`, which only waits for the dependency's container to start, not for it to actually be ready to accept connections — a database container starts almost instantly but may take several more seconds before it accepts connections. Also watch the health check's own timing: too short an interval or too few retries can mark a slow-starting service unhealthy before it's had a fair chance to come up.