Container health checks
Learn to manage container health checks in Docker, including configuration, troubleshooting, and practical application for robust container orchestration.
Focus: manage container health checks
Ever deployed a container that seemed healthy but silently stopped serving traffic? That's the pain this lesson solves. Without a health check, Docker only knows if the container's main process is running, not if your app is actually responding correctly. You'll learn to manage container health checks so your orchestrator (and you) can detect and restart truly broken containers automatically.
The problem this lesson solves
A running container doesn't mean a working application. A web server might be alive but hung on a deadlock, a database process might be running but refusing connections, or an API might be crash-looping faster than Docker's default restart policy can catch. The core problem: Docker's default view is process-centric (PID 1 alive? Container is up). Production systems need application-centric awareness — "is my service healthy enough to handle traffic?"
Without manage container health checks, you'll: - Restart containers that are fine, or fail to restart containers that are silently broken. - Waste time debugging "running" containers that don't serve requests. - Leave orchestration tools (Docker Swarm, Compose, Kubernetes) blind to real failures.
Core concept / mental model
Think of a health check as a contract signed by your container. Every few seconds, Docker runs a command inside the container. If that command succeeds (exit code 0), the container is marked healthy. If it fails repeatedly, Docker marks it unhealthy and can auto-restart it.
Key definitions
- HEALTHCHECK: Dockerfile instruction that defines the check command, interval, timeout, retries, and start period.
- health status:
starting→healthy→unhealthy(three-state model). - start period: Grace time before Docker begins counting failures — critical for slow-booting containers (e.g., databases, Java apps).
┌──────────────┐ success ┌──────────────┐
│ starting │ ▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸ │ healthy │
└──────────────┘ └──────────────┘
│ │
│ failure │ failure
▼ ▼
┌──────────────┐ ┌──────────────┐
│ unhealthy │ ◂◂◂◂◂◂◂◂◂◂◂◂◂◂◂◂│ (restart) │
└──────────────┘ └──────────────┘
Pro tip: Always set a
start_periodfor apps that need time to initialize — without it, a slow boot counts as repeated failures and flips tounhealthyprematurely.
How it works step by step
The health check lifecycle in Docker:
- Container starts: Docker immediately sets the status to
starting. - After
start_periodexpires: Docker runs theHEALTHCHECKcommand for the first time. - Every
intervalseconds: Docker re-runs the command. - If command exits 0 (success): Status becomes
healthy. - If command exits non-zero: Docker increments a failure counter.
- When failures reach
retries: Status changes tounhealthy. - If unhealthy and
--restart=always: Docker restarts the container.
The HEALTHCHECK syntax
HEALTHCHECK [OPTIONS] CMD command
Options:
--interval=DURATION (default 30s)
--timeout=DURATION (default 30s)
--start-period=DURATION (default 0s)
--retries=N (default 3)
Blockquote insight: The health check runs inside the container's namespaces — it has access to localhost and files. Use
curlorwgetonlocalhost, not on a public IP.
Hands-on walkthrough
Let's build a real health-checked container from scratch.
Example 1: Basic health check with curl
Create a Dockerfile for a simple HTTP server:
FROM python:3.11-alpine
RUN apk add --no-cache curl
COPY server.py /server.py
HEALTHCHECK --interval=5s --timeout=3s --start-period=10s --retries=2 \
CMD curl -f http://localhost:8080/health || exit 1
CMD ["python", "-m", "http.server", "8080"]
Build and run:
docker build -t health-check-demo .
docker run -d --name health-demo --restart=always health-check-demo
docker inspect health-demo --format='{{json .State.Health}}' | jq .
Expected output (after 10–20 seconds):
{
"Status": "healthy",
"FailingStreak": 0,
"Log": [
{
"Start": "2025-01-01T12:00:10Z",
"End": "2025-01-01T12:00:13Z",
"ExitCode": 0,
"Output": ""
}
]
}
Example 2: Simulating a failing health check
Now break the endpoint — remove the /health route (in a real app) or stop the container's internal process. Watch the health status degrade:
docker exec health-demo sh -c "kill -STOP 1"
docker inspect health-demo --format='{{.State.Health.Status}}'
# Output: unhealthy (after retries)
Docker will restart the container because we used --restart=always. Check logs:
docker logs health-demo --tail 5
Example 3: Health check for a database
PostgreSQL health check (no curl needed — check with pg_isready):
FROM postgres:16
HEALTHCHECK --interval=10s --timeout=5s --start-period=30s --retries=5 \
CMD pg_isready -U postgres || exit 1
Run and verify:
docker build -t pg-health .
docker run -d --name pg-db -e POSTGRES_PASSWORD=secret pg-health
docker inspect pg-db --format='{{.State.Health.Status}}'
# Output: starting → healthy (after ~30 seconds)
Pro tip: For databases, always set a generous
start_period(30–60s) because initial startup involves WAL recovery, index creation, and buffer pool warm-up.
Example 4: Custom health check in docker-compose.yml
Health checks can also be defined in Compose (no need to rebuild the image):
version: "3.9"
services:
web:
image: nginx:latest
healthcheck:
test: ["CMD", "nginx", "-t"]
interval: 10s
timeout: 5s
retries: 3
start_period: 5s
restart: always
Start and check status:
docker compose up -d
docker compose ps
# The STATUS column shows "healthy" or "unhealthy"
Compare options / when to choose what
| Approach | Pros | Cons | Best for |
|---|---|---|---|
Dockerfile HEALTHCHECK |
Built into image, portable | Requires rebuild to change | Stateless apps (web APIs, workers) |
docker-compose.yml healthcheck |
Changeable without rebuild, per-service | Only for Compose projects | Compose-based dev/staging |
Docker API healthcheck endpoint |
External, no image change | Out-of-band, less integrated | Advanced orchestration |
When to choose what: - Use Dockerfile HEALTHCHECK when you own the image and want health logic baked in. - Use Compose healthcheck when you're composing third-party images and can't modify their Dockerfile. - Use external probes (like curl from another container) when you need health from outside the container's network namespace.
Troubleshooting & edge cases
Common health check failures
-
Health check command not found inside container
text Error from health check: "executable file not found in $PATH"Fix: Install the tool (e.g.,apk add curl) or use a command that exists — likepython -c "..."instead ofwget. -
Health check never becomes healthy - Check the log:
docker inspect <container> --format='{{json .State.Health.Log}}' | jq .[-1]- Ensure the port/protocol is correct inside the container (uselocalhost, not127.0.0.1for IPv6 configurations). - Increasestart_period— especially for Java apps with JVM warmup. -
Container constantly restarts - If
retriesis too aggressive (e.g., 1), a transient blip kills the container. - Setretriesto at least 3 for typical web apps, 5 for databases. -
curlhanging forever - The--timeoutflag is mandatory in HEALTHCHECK; if omitted, Docker's internal timeout may not kill a hanging curl. - Always usecurl -f --max-time 5 http://...to ensure fast failure.
Pro tip: Use
docker eventsto see health status changes in real time:bash docker events --filter 'container=health-demo' --filter 'event=health_status'
What you learned & what's next
You now understand how to manage container health checks — from Dockerfile syntax to Compose configuration, from basic HTTP checks to database probes. You can:
- Define HEALTHCHECK instructions for custom images.
- Override health checks in docker-compose.yml without rebuilding.
- Troubleshoot common health check failures (missing tools, wrong ports, start period misconfiguration).
- Monitor container health status using docker inspect and docker events.
Next lesson: Dive into container restart policies — how to pair health checks with restart: always or restart: unless-stopped for true self-healing deployments.
Remember: A running container isn't enough — a healthy container is.
Practice recap
Create a Dockerfile for a simple Flask app that echoes 'healthy' on /health. Add a HEALTHCHECK with 10s interval, 5s timeout, 30s start period, and 3 retries. Build and run it. Then deliberately break the app by removing the /health route — watch Docker restart the container. This reproduces a real production scenario where apps silently fail.
Common mistakes
- Forgetting to install the health check tool (e.g., curl, wget) inside the container — leads to
executable file not founderrors. - Setting
start_periodtoo short (or zero) for slow-booting containers — causes immediateunhealthyflips during initial startup. - Using public IP or hostname in health check instead of
localhost— the check runs inside the container's network namespace, so public DNS may fail or route outside. - Omitting the
--timeoutflag in curl/wget — if the check hangs forever, Docker may take minutes to declareunhealthy. - Setting
retriestoo low (e.g., 1) — a single transient failure restarts a container unnecessarily.
Variations
- Use
wgetinstead ofcurl(e.g.,wget -q --timeout=3 http://localhost/ -O /dev/null) — smaller footprint on Alpine-based images. - Define health checks in
docker-compose.ymlwithout touching the Dockerfile — useful when using third-party images you don't control. - For gRPC or custom protocols, use a Python script inside the container as the health check command instead of curl.
Real-world use cases
- Production web API — curl endpoint /health returns 200 only when DB and cache are reachable; Docker restarts on failure.
- Database cluster (e.g., PostgreSQL) — use
pg_isreadyor custom SQL query to verify the primary is accepting writes. - Background worker in a queue system — check that a PID file exists and the process is actively processing jobs every 30s.
Key takeaways
- Health checks provide application-level awareness — not just process-is-running, but application-is-healthy.
- Always set
start_periodfor containers with boot time (databases, Java apps, complex services) to avoid false positives. - Use
--retriesto tune sensitivity — higher values (3–5) for transient systems, lower (1–2) for essential services that should restart fast. - Define health checks in Dockerfile for portability, or override in docker-compose.yml when using third-party images.
- Inspect health status with
docker inspect --format='{{.State.Health.Status}}'and monitor events withdocker events. - The health check command runs inside the container — always use
localhostor127.0.0.1, not hostnames.
Keep learning
Related tutorials, quizzes, and articles for this topic.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.