Docker Health Checks in Compose
Learn how to define and use health checks in Docker Compose to monitor container health, ensure service readiness, and handle failures automatically.
Focus: use docker health checks in compose
You've built a multi-service application with Docker Compose, but how do you know your containers are actually healthy? A container can be running its main process but be in a broken state—unable to accept connections, stuck in a boot loop, or waiting on a resource that never comes. That's where Docker health checks come in. They let you define a probe—a command or HTTP request—that Docker runs periodically to verify your service is truly alive and ready. In this lesson, you'll learn how to use docker health checks in compose to make your services self-healing and your deployments more reliable.
The problem this lesson solves
Many developers assume that if a container is running (docker ps shows Up), the service inside is fully functional. But that's a dangerous assumption. A web server might start in seconds but take another ten seconds to load configuration or connect to a database. Clients hitting the server during that window get connection errors, timeouts, or 502 Bad Gateway responses.
Consider a typical web app stack: a Node.js API fronted by Nginx, with a PostgreSQL database. The API container might start before PostgreSQL is ready, causing repeated connection retries inside the app. Nginx, which depends only on condition: service_started (the default), begins routing traffic to a half-baked API. The result? A cascade of failures that are hard to debug because every service appears "running."
Docker health checks solve this by giving you a programmatic, configurable way to test whether a service is ready to do actual work—not just whether its process is alive.
Core concept / mental model
Think of a health check as a janitor that checks on your service at regular intervals. If the janitor finds the service unresponsive (the probe fails), Docker marks the container as unhealthy. You can then configure other services to wait for a healthy state before starting, or let the orchestrator restart the unhealthy container automatically.
A health check has three key parameters:
test— A command or HTTP request that returns exit code 0 (healthy) or 1+ (unhealthy). Common tests includecurl http://localhost:8080/healthor a simple shell command likepg_isready.interval— How often the check runs (e.g.,30s).retries— How many consecutive failures are needed to mark the container asunhealthy.start_period— A grace period after the container starts during which Docker won't count failures fromretries. This prevents false positives during initialization.
Pro tip: Always use
start_periodto avoid marking a container unhealthy during normal startup. Without it, a slow boot could trigger a restart loop.
How it works step by step
Here's the lifecycle of a health check in Compose:
- Container starts — Docker creates the container and starts its main process. The health check does not run until the
start_periodends (if defined) or immediately (ifstart_periodis omitted). - First probe — After the start period (or at the first
interval), Docker executes thetestcommand inside the container. - If the command exits with code 0 (success), the state ishealthy. - If it exits with code 1 (failure), Docker increments a failure counter. - Repeat on interval — Every
intervalseconds, Docker runs the probe again. - Failure threshold — If the failure counter reaches
retries, the container's status becomesunhealthy. - Automatic recovery — If the container becomes healthy again (probe succeeds), the failure counter resets and the status changes back to
healthy.
In Docker Compose, you can also use depends_on: condition: service_healthy to make a service wait until a dependency reports healthy. This is much more reliable than relying on startup order alone.
Hands-on walkthrough
Let's build a practical example. We'll create a simple Python Flask app that simulates a service with a health endpoint. Then we'll define a health check in Compose that uses curl to probe that endpoint.
Step 1: Create the Flask app
# app.py
from flask import Flask, jsonify
import time
import os
app = Flask(__name__)
# Simulate a slow startup
start_time = time.time()
@app.route('/')
def home():
return jsonify({"status": "ok", "service": "flask-app"})
@app.route('/health')
def health():
# Simulate health logic: return unhealthy if uptime < 10 seconds
if time.time() - start_time < 10:
return jsonify({"status": "not ready"}), 503
return jsonify({"status": "healthy"})
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000)
# Dockerfile
FROM python:3.10-slim
WORKDIR /app
COPY app.py .
RUN pip install flask
EXPOSE 5000
CMD ["python", "app.py"]
Step 2: Define Compose services with health checks
# docker-compose.yml
version: '3.8'
services:
flask-app:
build: .
ports:
- "5000:5000"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:5000/health"]
interval: 10s
timeout: 5s
retries: 3
start_period: 15s
restart: unless-stopped
watcher:
image: busybox
command: /bin/sh -c "while true; do sleep 5; done"
depends_on:
flask-app:
condition: service_healthy
healthcheck:
test: ["CMD", "true"]
# Expected behavior
# - Flask app starts and becomes healthy after ~12 seconds
# - watcher waits for flask-app to become healthy before starting
# - If flask-app health check fails 3 times, Docker restarts it
Step 3: Run and observe
docker compose up -d
docker compose ps
# Wait a few seconds, then check health status
docker compose exec flask-app curl -s http://localhost:5000/health
docker inspect --format='{{json .State.Health.Status}}' flask-app
The output of docker compose ps will show healthy under the STATUS column for flask-app once the health check passes. If something goes wrong, you'll see unhealthy or starting until the start period ends.
Compare options / when to choose what
| Approach | Description | Best for | Pros | Cons |
|---|---|---|---|---|
| CMD check | Runs a command inside the container (e.g., curl, pg_isready). |
Web apps, databases | Simple, flexible, any image | Requires tooling (curl) in container |
| NONE | Disables health checks entirely. | Stateless, fast-start services | No overhead | No failure detection |
| · | Custom script | Complex readiness logic | Full control, multiple conditions | More code to maintain |
| HTTP health endpoint | App provides /health route returning 200/503. |
Microservices, APIs | Aligns with app logic, zero external deps | Requires app modification |
Pro tip: For production services, prefer HTTP health endpoints over
curlcommands because they can include application-specific checks (database connectivity, cache warmup, etc.).
Troubleshooting & edge cases
Common mistakes
- Missing tools in the container — If your base image lacks
curlorwget, the health check will fail. Always useCMD-SHELL(orCMDwith an array) and ensure the tool exists. For Alpine-based images, installcurlexplicitly. - Too aggressive
retries— Settingretries: 1with a shortintervalcan cause flapping (unhealthy/healthy cycles). Useretries: 3or more, and set a reasonablestart_period. - Forgetting
start_period— Without it, a slow database or cache warmup can trigger unhealthy status and restart loops. - Health check on a non-Linux process — health checks run inside the container's namespaces. They cannot reach external hosts or services unless you pass network access.
- Using
depends_onwithoutcondition: service_healthy— The defaultdepends_ononly waits for container start, not health. Always specify the condition.
Edge cases
- Health check restart policy — If a service's health fails and
restartis set tounless-stoppedoralways, Docker will restart the container. This can lead to restarts if the health check is misconfigured. - Multiple services with same health endpoint — If two containers share a network (e.g., default bridge), health checks from one can accidentally probe the other. Use
localhostor container IP within the same Compose network.
What you learned & what's next
You now understand how to define health checks in Docker Compose to verify container health, use start_period to avoid false positives, and let dependent services wait until dependencies are truly healthy. You also know the differences between CMD checks, HTTP endpoints, and NONE strategies.
Learning objectives achieved:
- Explain how health checks work as a monitoring probe inside containers.
- Write a Compose file with healthcheck and depends_on: condition: service_healthy.
Next lesson: You'll learn how to use Docker's init process inside containers to handle zombie processes and signal forwarding—essential for running robust services in production.
Practice recap
Create a new Compose file that adds a Redis service with a health check using redis-cli ping. Then update your Flask app's depends_on to wait for Redis to report healthy. Run docker compose up -d and verify both services show healthy in docker compose ps.
Common mistakes
- Using
healthcheckwithoutstart_period— causes containers to be marked unhealthy during normal startup if the service takes longer than the interval to become ready. - Forgetting to install
curlorwgetin a slim base image — the health check fails because the command doesn't exist, but Docker won't tell you why. - Setting
retries: 1with a shortinterval— creates flapping (rapid unhealthy/healthy cycles) and potential restart loops. - Relying on
depends_onwithoutcondition: service_healthy— services start in order but without waiting for health, defeating the purpose of the check.
Variations
- Use a custom shell script as the health check test for complex multi-condition probing (e.g., check DB + cache connectivity).
- Use
CMD-SHELLinstead ofCMDfor more readable commands (e.g.,test: ["CMD-SHELL", "curl -f http://localhost/health || exit 1"]). - Integrate health checks with orchestration (Kubernetes) where liveness/readiness probes serve a similar role.
Real-world use cases
- A microservice that depends on a database — health check verifies DB connectivity via a
/healthendpoint before accepting traffic. - A reverse proxy (Nginx) that waits for the upstream API to become healthy before routing clients, using
depends_on: condition: service_healthy. - A batch processing system that restarts automatically if health checks detect a stuck or unresponsive worker container.
Key takeaways
- Health checks let you confirm a container is truly ready, not just running — use them to prevent cascading failures.
- Always set
start_periodto avoid false positives during application initialization. - Use
depends_on: condition: service_healthyto delay a service until its dependency is healthy. - Choose between CMD checks, HTTP endpoints, or custom scripts based on whether you want external tools or app-level logic.
- Keep
retriesto 3 or more and set a realisticintervalto avoid flapping.
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.