July 12, 2026 · Docker HQ Team

Docker Health Checks Explained, With Examples

A Docker health check is a command Docker runs inside a running container at a set interval to decide whether the app inside is actually working. It's how you tell the difference between "the process is running" and "the process is serving traffic." You define it with the HEALTHCHECK instruction in a Dockerfile or a healthcheck block in Compose, and Docker reports the result as starting, healthy, or unhealthy.

A container can be Up for hours while the app inside it has deadlocked, run out of database connections, or started returning 500s. The process didn't crash, so Docker thinks everything is fine. A health check catches that gap by testing the thing you actually care about: can the app respond?

How Docker health checks work

Docker runs your health check command on a schedule. If the command exits 0, the container is healthy. If it exits 1, it's unhealthy. Exit code 2 is reserved, so don't use it.

There's a startup grace period too. When a container first boots, its status is starting. Failures during the start-period don't count against it, which stops Docker from killing a container that just needs 20 seconds to warm up a JVM or run migrations.

You can inspect the current state at any time:

docker inspect --format '{{.State.Health.Status}}' my-container

And you can read the recent probe history, including stdout from each check, which is the first place to look when a check is flapping:

docker inspect --format '{{json .State.Health}}' my-container | jq

The four tuning flags

Every health check has the same knobs, whether you set them in a Dockerfile or Compose:

  • --interval (default 30s): how often the check runs.
  • --timeout (default 30s): how long a single check can run before it counts as a failure.
  • --start-period (default 0s): grace window at startup where failures don't count.
  • --retries (default 3): consecutive failures before the container flips to unhealthy.

So with the defaults, a container needs three failed checks in a row, spaced 30 seconds apart, before Docker marks it unhealthy. That's a minute and a half of downtime before the status even changes. For anything user-facing, tighten it.

Writing a health check in a Dockerfile

The most common case is an HTTP service. Point the check at a lightweight endpoint that touches the parts of your app you care about:

HEALTHCHECK --interval=15s --timeout=3s --start-period=30s --retries=3 \
  CMD curl -f http://localhost:8080/healthz || exit 1

The -f flag makes curl return a non-zero exit code on HTTP errors like 500, so a failing app fails the check. Without -f, curl happily exits 0 after receiving a 503 and your check is useless.

If your image is slim and doesn't ship curl, don't install it just for this. Use whatever the runtime already provides. A Node image can hit its own endpoint:

HEALTHCHECK --interval=15s --timeout=3s CMD \
  node -e "fetch('http://localhost:3000/healthz').then(r => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))"

For a database, run the tool that ships with it. Postgres has pg_isready:

HEALTHCHECK --interval=10s --timeout=5s --retries=5 \
  CMD pg_isready -U postgres || exit 1

To remove a health check inherited from a base image, set it to NONE:

HEALTHCHECK NONE

Health checks in Docker Compose

Compose gives you the same controls in YAML, and this is where health checks earn their keep, because you can gate service startup on them. The test array's first element is either CMD (exec form, no shell) or CMD-SHELL (runs through /bin/sh, so you can use || and env vars):

services:
  api:
    image: myapp:latest
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/healthz"]
      interval: 15s
      timeout: 3s
      retries: 3
      start_period: 30s

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

Now the part that actually saves you at 3am. Use depends_on with condition: service_healthy so your API doesn't start until the database passes its check:

  api:
    depends_on:
      db:
        condition: service_healthy

Without this, depends_on only waits for the container to start, not for Postgres to accept connections. Your API boots, tries to connect, and dies. People paper over that with retry loops and sleep hacks in entrypoints. A service_healthy condition does it properly.

What makes a good health endpoint

A health check should test that the app can do its job, and nothing more. Two failure modes to avoid:

Too shallow. A route that returns 200 OK without checking anything just confirms the web server accepts connections. That's a liveness signal, and it's fine, but call it what it is.

Too deep. If your /healthz runs three database queries, calls two upstream APIs, and checks a cache, you've built a check that fails whenever any dependency hiccups. Now a slow third-party API marks your perfectly healthy container as unhealthy, and if you've wired that to a restart policy or orchestrator, you get a restart storm. A brief network blip becomes an outage you caused.

The middle ground: check the dependencies your app genuinely can't run without (usually its own database), keep the check cheap, and give it a short timeout. Return a plain status, log the reason on failure, and don't cascade.

Acting on unhealthy status

Here's the catch that trips people up. Plain Docker marks a container unhealthy, but it does not restart it. The status is just a label. restart: unless-stopped reacts to the process exiting, not to a failed health check.

If you want automatic restarts on health failure, you need an orchestrator. Docker Swarm reschedules unhealthy tasks, and Kubernetes does the same with its own liveness and readiness probes (which are a separate mechanism, not the Dockerfile HEALTHCHECK). On a single host with Compose, the health status is a signal you have to watch or wire up yourself.

Which raises the real question: who's watching? A container can go unhealthy at 2am and sit there until someone notices the errors. Polling docker ps from your laptop isn't a plan. That's the gap Docker HQ fills. It watches container health across your remote hosts over SSH and pushes an alert to your phone the moment a container flips to unhealthy or a host goes dark. You hear about it from a notification, not from your users. If you want the full picture of what to track beyond health status, our guide to Docker container monitoring walks through the metrics that matter.

When a container does go unhealthy, the health log is your starting point, but the cause is often elsewhere. A container that keeps failing checks and restarting is frequently getting killed for memory, which shows up as exit code 137 / OOMKilled. And a host that's run out of disk will fail health checks across every container at once, so it's worth knowing how to clean up Docker disk usage safely before it bites.

Frequently asked questions

What's the difference between a Docker health check and a restart policy?

A restart policy (restart: always, unless-stopped, on-failure) reacts to the container's main process exiting. A health check reacts to your custom command failing. A process can stay alive while the app is broken, and a restart policy won't touch it. The health check catches that case, but on plain Docker it only sets a status, it doesn't trigger the restart. You need Swarm or Kubernetes for that.

Why is my container stuck in the "starting" state?

A container stays starting during the start-period and until the first check passes. If it never leaves starting, either your start-period is very long, or the check has never once succeeded. Run the check command manually inside the container with docker exec and see what it returns. A common cause is the check pointing at a port or path the app doesn't actually serve.

Does a health check affect container performance?

Rarely enough to matter, if the check is cheap. It runs your command inside the container on each interval, so a curl to a local endpoint every 15 seconds is negligible. It becomes a problem when the check itself is expensive, like running heavy database queries every few seconds. Keep the check light and the interval sane.

Can I add a health check to a container that's already running?

Not to an existing container, the health check is baked in at create time. You set it in the Dockerfile, in Compose, or with docker run --health-cmd. To change it, recreate the container. For a quick one-off test without rebuilding the image:

docker run -d --name test \
  --health-cmd='curl -f http://localhost:8080/healthz || exit 1' \
  --health-interval=15s myapp:latest

Start with the dependency you can't run without

If you're adding your first health check, don't overthink the endpoint. Pick the one dependency your app dies without, usually its database, write a check that confirms that connection works, give it a 3 second timeout and a 30 second start period, and ship it. Then decide who gets paged when it fails, because a health check nobody watches is just a nicer-looking log line.