Keeping containers running isn't enough; you need to know they're actually healthy. Learn how to configure, observe, and act on Docker health checks with practical snippets you can lift into your own projects.
Keeping containers “running” isn’t enough; you need to know they’re actually healthy. Docker health checks give you that signal, and Docker Compose makes them easy to configure, observe, and act on. Here’s how they work, with practical snippets you can lift into your own projects.
What is a health check?
A health check is a command Docker runs inside a container on a schedule. If it succeeds (exit code 0), the container is marked healthy. If it fails or times out too many times, the container is unhealthy. Compose just passes these settings through to Docker.
The core pieces
- Command: What to run to verify health (often a curl to an HTTP endpoint).
- Interval: How often to run it.
- Timeout: How long to wait before considering it failed.
- Retries: How many consecutive failures before marking unhealthy.
- Start period: Grace period before counting failures (useful during startup).
Defining a health check in Dockerfile
# Dockerfile
HEALTHCHECK \
CMD curl -f http://localhost:8080/health || exit 1Tips:
- Ensure the checker tool (e.g.,
curl) is installed in the runtime image. - Use the correct path (if your app is under
/api, hit/api/health). - Keep the command cheap: avoid heavy queries or migrations.
Defining a health check in docker-compose.yml
Compose also supports healthchecks per service. This is handy if you don’t control the image or want different settings per environment.
services:
web:
image: myapp:latest
ports: ["8080:8080"]
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:8080/api/health || exit 1"]
interval: 30s
timeout: 5s
retries: 3
start_period: 10sNotes:
CMD-SHELLlets you use shell features (|| exit 1).- You can also use
CMDwith an array if no shell logic is needed.
Reading health status
docker ps or docker compose ps shows healthy, unhealthy, or starting.
Detailed log of the health checks:
docker inspect --format='{{json .State.Health}}' <container_name> | jqYou’ll see each attempt’s exit code and output, which is great for debugging “curl: not found” or 404s.
Common pitfalls (and fixes)
Missing curl
Symptom: health log shows /bin/sh: 1: curl: not found
Fix: install curl in the runtime image (apt-get install -y curl) or use wget -qO-.
Wrong path
Symptom: 404s in health logs.
Fix: hit the real endpoint (e.g., /api/health if your app has a global /api prefix).
Too aggressive timing
Symptom: app marked unhealthy during startup.
Fix: raise start_period, or increase retries/interval.
Heavy checks
Symptom: timeouts.
Fix: keep health checks lightweight; avoid DB migrations or big queries.
Using health in dependencies
Compose’s depends_on can wait for health (v3.9+ syntax):
services:
api:
image: myapi:latest
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
interval: 20s
timeout: 5s
retries: 3
start_period: 10s
worker:
image: myworker:latest
depends_on:
api:
condition: service_healthyNow worker starts only after api reports healthy.
When to put it in Dockerfile vs Compose
- Dockerfile: Good default for everyone using the image; travels with the artifact.
- Compose: Good for environment-specific checks or when you can’t change the image.
You can combine them: Dockerfile defines a default; Compose overrides for local dev or staging.
Minimal templates to copy-paste
HTTP app (Dockerfile)
HEALTHCHECK \
CMD curl -f http://localhost:8080/health || exit 1HTTP app (Compose override)
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:8080/api/health || exit 1"]
interval: 30s
timeout: 5s
retries: 3
start_period: 15sTCP check without curl (e.g., Redis)
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 30s
timeout: 3s
retries: 3Debug checklist
docker compose psto see current health.docker inspect ...Healthto read the exact failure/output.- Run the test command manually inside the container:
docker compose exec web sh -lc "curl -f http://localhost:8080/api/health"- Adjust path, install the tool, or relax timing, then rebuild/restart.
Bottom line
Health checks are just small, repeated commands. Keep them light, point them at the right endpoint, ensure the tooling exists, and give your app enough time to start. With Docker Compose, you get clear status, dependency gating, and straightforward debugging.
FAQ
- What's the difference between a container running and being healthy?
- A container can be running but still broken. Maybe the app crashed, the port isn't listening, or a critical dependency failed. Health checks actively test that your service responds correctly, not just that the process exists.
- Should I define health checks in the Dockerfile or docker-compose.yml?
- Dockerfile is best for defaults that travel with the image. Use docker-compose.yml to override for environment-specific needs (dev vs staging) or when you can't modify the upstream image.
Welcome to The infinite monkey theorem
Somewhere a monkey just typed Shakespeare in TypeScript. Be the first to read the masterpieces (and the hilarious misfires) landing on the blog.

