Maintenance

Site is under maintenance — quizzes are still available.

Go to quizzes

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

Sponsored

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 include curl http://localhost:8080/health or a simple shell command like pg_isready.
  • interval — How often the check runs (e.g., 30s).
  • retries — How many consecutive failures are needed to mark the container as unhealthy.
  • start_period — A grace period after the container starts during which Docker won't count failures from retries. This prevents false positives during initialization.

Pro tip: Always use start_period to 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:

  1. Container starts — Docker creates the container and starts its main process. The health check does not run until the start_period ends (if defined) or immediately (if start_period is omitted).
  2. First probe — After the start period (or at the first interval), Docker executes the test command inside the container. - If the command exits with code 0 (success), the state is healthy. - If it exits with code 1 (failure), Docker increments a failure counter.
  3. Repeat on interval — Every interval seconds, Docker runs the probe again.
  4. Failure threshold — If the failure counter reaches retries, the container's status becomes unhealthy.
  5. 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 curl commands 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 curl or wget, the health check will fail. Always use CMD-SHELL (or CMD with an array) and ensure the tool exists. For Alpine-based images, install curl explicitly.
  • Too aggressive retries — Setting retries: 1 with a short interval can cause flapping (unhealthy/healthy cycles). Use retries: 3 or more, and set a reasonable start_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_on without condition: service_healthy — The default depends_on only waits for container start, not health. Always specify the condition.

Edge cases

  • Health check restart policy — If a service's health fails and restart is set to unless-stopped or always, 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 localhost or 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 healthcheck without start_period — causes containers to be marked unhealthy during normal startup if the service takes longer than the interval to become ready.
  • Forgetting to install curl or wget in a slim base image — the health check fails because the command doesn't exist, but Docker won't tell you why.
  • Setting retries: 1 with a short interval — creates flapping (rapid unhealthy/healthy cycles) and potential restart loops.
  • Relying on depends_on without condition: service_healthy — services start in order but without waiting for health, defeating the purpose of the check.

Variations

  1. Use a custom shell script as the health check test for complex multi-condition probing (e.g., check DB + cache connectivity).
  2. Use CMD-SHELL instead of CMD for more readable commands (e.g., test: ["CMD-SHELL", "curl -f http://localhost/health || exit 1"]).
  3. 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 /health endpoint 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_period to avoid false positives during application initialization.
  • Use depends_on: condition: service_healthy to 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 retries to 3 or more and set a realistic interval to avoid flapping.

Sponsored

Sponsored

Discussion

Questions, corrections, and tips help everyone reading this page.

0 comments

Add a comment

Shown publicly with your comment.

Be constructive · max 4,000 characters

No comments yet — start the thread.

Related tutorials, quizzes, and articles for this topic.