Healthcheck #
A healthcheck is the mechanism for determining whether a service inside a container is truly healthy — not just that its process is running, but that it’s ready to accept requests and respond correctly. Without a healthcheck, you can’t tell a running-but-broken container apart from a fully functional one.
This article covers healthcheck syntax in Compose, how to write effective test commands, and best practices for health monitoring.
What Is a Healthcheck? #
A healthcheck is a command run periodically inside the container by the Docker daemon. The exit code determines the status:
- 0 — healthy
- 1 — unhealthy
- 2 — reserved (unused)
Healthcheck status is visualized in docker compose ps:
NAME SERVICE STATUS PORTS
myapp-web-1 web Up 5 minutes (healthy) 0.0.0.0:80->80/tcp
myapp-api-1 api Up 5 minutes (healthy) 8080/tcp
myapp-db-1 db Up 5 minutes (healthy)
If the healthcheck fails, the status shows (unhealthy). The container doesn’t necessarily restart — that depends on the restart policy.
Healthcheck Syntax #
The healthcheck field sits under a service.
services:
api:
image: myapi
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:8080/health || exit 1"]
interval: 30s
timeout: 10s
retries: 3
start_period: 30s
Healthcheck fields #
| Field | Default | Description |
|---|---|---|
test | (required) | The test command |
interval | 30s | Interval between tests |
timeout | 30s | Per-test timeout |
retries | 3 | Failures before unhealthy |
start_period | 0s | Initial grace period |
Test Command Formats #
CMD exec form (no shell):
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
The CMD exec form doesn’t run a shell. Arguments are passed directly to the executable. Cleaner, but can’t use shell features (pipes, env var expansion, etc.).
CMD-SHELL form (with shell):
test: ["CMD-SHELL", "curl -f http://localhost:8080/health || exit 1"]
CMD-SHELL runs the command in a shell (/bin/sh -c). Shell features work. Add || exit 1 to guarantee exit code 1 on failure.
NONE (disable):
test: ["NONE"]
Disables an inherited healthcheck from the image.
CMD vs CMD-SHELL: UseCMDwhen the test binary accepts arguments directly. UseCMD-SHELLwhen you need shell logic (pipes, conditionals, env vars). For HTTP checks,CMD-SHELLis usually more flexible.
Healthchecks for Popular Services #
HTTP Services #
api:
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:8080/health || exit 1"]
interval: 30s
timeout: 10s
retries: 3
The /health endpoint must return 200 when the service is ready. It can be more specific: /health/live (process running) vs /health/ready (ready to accept requests).
Postgres #
db:
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres -d myapp"]
interval: 10s
timeout: 5s
retries: 5
pg_isready is the official Postgres tool for checking whether the server accepts connections. Returns 0 when ready, 3 when not ready.
MySQL #
db:
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p$MYSQL_ROOT_PASSWORD"]
interval: 10s
timeout: 5s
retries: 5
Redis #
cache:
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 3
redis-cli ping returns “PONG” (interpreted as exit 0).
MongoDB #
db:
healthcheck:
test: ["CMD", "mongo", "--eval", "db.runCommand({ping: 1})"]
interval: 10s
timeout: 5s
retries: 5
RabbitMQ #
mq:
healthcheck:
test: ["CMD", "rabbitmq-diagnostics", "ping"]
interval: 30s
timeout: 10s
retries: 3
start_period: 60s
start_period of 60s gives RabbitMQ time to boot.
Nginx #
nginx:
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "http://localhost/health"]
interval: 30s
timeout: 10s
retries: 3
Custom Health Endpoints #
Applications you write should expose a /health endpoint returning 200 when healthy. Pattern:
# Flask
@app.route('/health')
def health():
# Check dependencies
try:
db.session.execute('SELECT 1')
return '', 200
except Exception:
return 'Database unavailable', 503
// Express
app.get('/health', (req, res) => {
// Check dependencies
db.ping()
.then(() => res.status(200).send('OK'))
.catch(() => res.status(503).send('DB down'));
});
Start Periods for Slow-Starting Services #
Some services take a long time to be fully ready. start_period provides a grace period during which failed healthchecks are ignored.
db:
image: postgres:16-alpine
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5
start_period: 60s # 60s grace period
During the first 60 seconds, failed healthchecks don’t count as unhealthy. After 60s, normal healthcheck behavior.
Important: start_period doesn’t delay the first healthcheck — it only changes when the “unhealthy” counter starts counting. Healthchecks still run and may fail early on.
Interval and Retries Tuning #
The defaults (interval 30s, retries 3) may not be optimal for every service.
Services needing quick detection:
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:8080/health || exit 1"]
interval: 5s
timeout: 2s
retries: 2
Detection within 5-10s when the service goes down. Fits services that must restart immediately on failure.
Services needing to ignore transient failures:
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:8080/health || exit 1"]
interval: 30s
timeout: 10s
retries: 5
start_period: 60s
Detection within 2-3 minutes, ignoring short failures. Fits services with occasional transient blips.
Integration with depends_on #
Healthchecks become powerful when combined with condition: service_healthy in depends_on.
services:
api:
depends_on:
db:
condition: service_healthy
cache:
condition: service_healthy
db:
image: postgres:16-alpine
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5
cache:
image: redis:7-alpine
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 3
api won’t start until both db and cache are healthy.
Restart Policies with Healthchecks #
When a container becomes unhealthy, the restart policy determines what happens.
services:
api:
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:8080/health || exit 1"]
interval: 30s
timeout: 10s
retries: 3
restart: on-failure # or unless-stopped
restart: on-failure restarts the container on non-zero exit codes. A failed healthcheck produces exit code 1 (via || exit 1), so the container restarts.
restart: unless-stopped always restarts unless manually stopped. More aggressive — “unhealthy” containers also get restarted.
Anti-Patterns: Useless Healthchecks #
Healthchecks That Always Pass #
# BAD
healthcheck:
test: ["CMD", "true"] # always returns 0
Healthchecks must genuinely test the service’s condition.
Healthchecks That Only Check the Process #
# BAD
healthcheck:
test: ["CMD-SHELL", "kill -0 1"] # checks whether PID 1 exists
A running process ≠ a ready service. A web server can run but be overloaded; a database can run but be locked.
Healthchecks That Are Too Complex #
# BAD
healthcheck:
test: ["CMD-SHELL", "complex multi-step check involving many services"]
Healthchecks must be lightweight and fast. If they take long, they’ll time out and always fail.
Test Commands Not in the Image #
# BAD — `curl` doesn't exist in alpine images
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost"]
Make sure the command exists in the image, or install it in the Dockerfile.
Liveness vs Readiness Probes #
A single Docker healthcheck differs from Kubernetes, which has separate liveness and readiness probes. But the concept can be adopted with multiple containers per service.
Liveness — is the process still alive? Restart if not. Readiness — is the service ready to accept traffic? Don’t send traffic if not ready.
In Docker Compose, you can simulate this with:
services:
app-liveness:
image: myapp
healthcheck:
test: ["CMD-SHELL", "kill -0 1 || exit 1"] # process running
interval: 30s
retries: 3
restart: on-failure
app-readiness:
image: myapp
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:8080/health || exit 1"]
interval: 5s
retries: 2
But this isn’t a clean pattern. For proper liveness + readiness, use Kubernetes or a platform supporting both probes.
Healthcheck Output Logging #
When a healthcheck fails, exit code 1 is logged. For debugging, add output to the test command.
healthcheck:
test: ["CMD-SHELL", "curl -v http://localhost:8080/health 2>&1 || exit 1"]
interval: 30s
timeout: 10s
retries: 3
Verbose output can be seen in docker inspect <container> or docker compose logs <service>.
Custom Healthcheck Scripts #
For complex tests, write a separate script and mount it into the container.
# healthcheck.sh
#!/bin/sh
set -e
# Check the process
kill -0 1 || exit 1
# Check the database connection
pg_isready -h db -U app || exit 1
# Check the Redis connection
redis-cli -h cache ping > /dev/null || exit 1
# Check an external API (if critical)
curl -f https://api.external.com/health > /dev/null 2>&1 || exit 1
exit 0
Mount it into the container and reference it in the healthcheck:
services:
app:
volumes:
- ./healthcheck.sh:/healthcheck.sh:ro
healthcheck:
test: ["CMD-SHELL", "sh /healthcheck.sh"]
interval: 30s
timeout: 15s
retries: 3
Custom script trade-offs: More powerful, can test multiple aspects. But more complex and can fail due to external factors (e.g. external APIs). For production, consider whether external dependencies should be part of the healthcheck or not.
Healthchecks and Observability #
Healthcheck status can be exported to monitoring systems.
Built-in Docker metrics:
# Healthcheck status
docker inspect --format='{{.State.Health.Status}}' <container>
# Last output
docker inspect --format='{{range .State.Health.Log}}{{.Output}}{{end}}' <container>
Prometheus + cAdvisor:
cAdvisor automatically exports the container_health metric to Prometheus. Alerts can be set for unhealthy states.
External monitoring:
For production services, external monitoring (Pingdom, Datadog, etc.) runs healthchecks from outside, so it can detect problems invisible from inside the container (network issues, host down, etc.).
Per-Container vs External Healthchecks #
| Aspect | Per-container (Docker) | External monitoring |
|---|---|---|
| Location | From inside the container | From outside the host |
| Detects | Broken internal services | Network, host, or service issues |
| Cost | Built-in, free | Extra tooling, possibly paid |
| Reliability | Depends on the health endpoint | Independent of the service |
For serious production, use both:
- Per-container healthchecks for
depends_onand restart policies. - External monitoring for alerting the team.
Recap Cheatsheet #
| Pattern | Example |
|---|---|
| HTTP healthcheck | ["CMD-SHELL", "curl -f http://localhost/health || exit 1"] |
| Postgres | ["CMD-SHELL", "pg_isready -U postgres"] |
| Redis | ["CMD", "redis-cli", "ping"] |
| RabbitMQ | ["CMD", "rabbitmq-diagnostics", "ping"] |
| Generic exec | ["CMD", "binary", "arg"] |
| Disable inherited | ["NONE"] |
| Slow startup | start_period: 60s |
| Quick detection | interval: 5s, retries: 2 |
| Tolerate transients | interval: 30s, retries: 5 |
A Complete Example: Production-Ready Healthchecks #
services:
nginx:
image: nginx:alpine
healthcheck:
test: ["CMD-SHELL", "wget -q --spider http://localhost/health || exit 1"]
interval: 30s
timeout: 5s
retries: 3
start_period: 10s
restart: unless-stopped
api:
build: ./api
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:8080/health || exit 1"]
interval: 15s
timeout: 5s
retries: 3
start_period: 30s
restart: unless-stopped
depends_on:
db:
condition: service_healthy
cache:
condition: service_healthy
db:
image: postgres:16-alpine
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app -d myapp"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
volumes:
- db-data:/var/lib/postgresql/data
restart: unless-stopped
cache:
image: redis:7-alpine
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 3
restart: unless-stopped
volumes:
db-data:
This stack has healthchecks on every service with appropriate timing. db has more retries (5) because Postgres sometimes needs longer to recover. api has a 30s start period for warmup. nginx only needs a 10s start period because it’s fast.
Summary #
- Healthchecks determine whether a service is ready, not just whether its process is running. The default
docker pscan’t tell the difference.- Syntax:
test: ["CMD-SHELL", "command"]is the most flexible.CMDexec form for direct binaries.- Fields:
test(required),interval,timeout,retries,start_period. Defaults fit most cases; tune as needed.- Integration with depends_on: use
condition: service_healthyso dependents start after the dependency is truly ready.- Restart policies:
on-failureorunless-stoppedfor auto-restart when unhealthy.- Anti-patterns: healthchecks that always pass, process-only checks, overly complex checks, or commands missing from the image.
- Best practices: a simple health endpoint in the application (return 200 when healthy), official tools for databases (pg_isready, redis-cli ping), tuned intervals/retries per service.
- Distinguish liveness vs readiness: liveness = process running (restart if down), readiness = ready for traffic (don’t route traffic).
- Healthcheck + load balancer = automatic removal from rotation when unhealthy (zero manual intervention).
- Monitor healthcheck failures via centralized logging/alerting, don’t rely on visual checks.
← Previous: Depends On & Startup Order Next: Local Development →