Docker & DevOps

Add health checks to Docker containers

Write Docker health checks in a Dockerfile or Compose file, read a container's health status, start services in order with service_healthy, and understand how health relates to restarts.

IntermediateLinux server5 min read

A running container is not the same as a working app. The process can be up while the app is stuck, still loading or unable to reach its database. A health check is a command Docker runs inside the container at intervals to find out. This guide shows how to write health checks, read the result and use it to start services in the right order.

Before you start

  • A Linux VPS with Docker Engine and the Compose plugin. See Install Docker and Docker Compose.
  • An app with something cheap to test, ideally an HTTP endpoint such as /health that returns 200 when the app and its dependencies are fine.
  • Knowledge of which tools exist inside the image. A check can only use commands the image contains, such as curl, wget or the app's own CLI.

How health checks work

Docker runs the check command inside the container on a schedule:

  • Exit code 0 means healthy.
  • Exit code 1 means unhealthy.

The container's status moves from starting to healthy, or to unhealthy after a set number of failures in a row. You see it in docker ps:

CONTAINER ID   IMAGE       STATUS
3f2c1ab9d0e4   myapp:1.0   Up 2 minutes (healthy)

Four settings control the timing:

  • interval: time between checks (default 30 seconds).
  • timeout: how long one check may take before it counts as failed (default 30 seconds).
  • retries: failures in a row before the status becomes unhealthy (default 3).
  • start_period: a grace period after start during which failures do not count, for apps that load slowly.

Add a health check in a Dockerfile

For an image you build yourself:

FROM node:22-alpine
WORKDIR /app
COPY . .
HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \
  CMD wget -q --spider http://127.0.0.1:3000/health || exit 1
CMD ["node", "server.js"]

Alpine images include BusyBox wget. Debian-based images often have neither curl nor wget, so install one in the Dockerfile or use a small script in the app's own language.

Add or override a health check in Compose

Compose lets you add checks to third-party images without rebuilding:

YAML
services:
  app:
    image: myapp:1.0
    healthcheck:
      test: ["CMD", "curl", "-fsS", "http://127.0.0.1:3000/health"]
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 20s

curl -f returns a non-zero exit code for HTTP errors such as 500, which is what you want.

Ready-made checks for common databases:

YAML
  db:
    image: postgres:16
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 10s
      timeout: 5s
      retries: 5

  redis:
    image: redis:7
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 3s
      retries: 5

Use CMD-SHELL when the test needs shell features such as || or variables. To turn off a check that an image defines, set healthcheck: { disable: true }.

Start services in order with service_healthy

By default depends_on only waits for a container to start, not to be ready. A web app often crashes because its database is still initialising. Make it wait for the health check instead:

YAML
services:
  app:
    image: myapp:1.0
    depends_on:
      db:
        condition: service_healthy
  db:
    image: postgres:16
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 10s
      retries: 5

docker compose up -d now starts db, waits until it reports healthy, then starts app. Add --wait to make the command itself wait until every service is healthy, which is useful in deploy scripts:

Bash
docker compose up -d --wait

Health checks and restarts

Plain Docker Engine does not restart a container just because it is unhealthy. Restart policies react only when the main process exits. Health status is used by:

  • depends_on with condition: service_healthy.
  • docker compose up --wait.
  • Reverse proxies such as Traefik, which can skip unhealthy containers.
  • Docker Swarm, which does replace unhealthy tasks.
  • Monitoring tools that read the status.

If you need automatic recovery, make the app exit when it detects it is broken so its restart policy kicks in, or alert on unhealthy containers with an external monitor such as Uptime Kuma.

Read the health status

Bash
docker inspect --format '{{json .State.Health}}' app

The output includes the current status, the failing streak and the last five check results with their output. That output is usually the fastest way to see why a check fails.

Check it works

Watch the status change after a start:

Bash
docker compose up -d
docker compose ps

The STATUS column should show (health: starting) and then (healthy) once the start period and first checks pass. To test the unhealthy path, stop the app's dependency, such as the database, and watch the app's status turn (unhealthy) after the configured retries.

Troubleshooting

  • Status stays starting then turns unhealthy: run the test command yourself with docker exec app curl -fsS http://127.0.0.1:3000/health and read the error.
  • exec: "curl": executable file not found: the image lacks curl. Use wget, a language-native check or install curl in your Dockerfile.
  • Check fails on localhost but the app works: the app listens only on IPv4 while localhost resolves to IPv6 first. Use 127.0.0.1.
  • Checks pile up and use CPU: the interval is too short or the check is expensive. Keep checks light and every 10–30 seconds.

Next steps

  • Keep containers up after crashes: Docker restart policies.
  • Use health checks for safer releases: Near zero-downtime deploys with Docker Compose.
Was this guide helpful?

Still stuck?

Ask MdmHost AI for a quick answer, or open a ticket and our team will take a look. We are here 24/7.