All posts

Docker Healthcheck: Full Syntax Guide + Why It Misses Cron

August 22, 2026

What Is a Docker Healthcheck?

A Docker healthcheck is an instruction added to a Dockerfile (or a docker-compose.yml service) that tells Docker how to test whether the application inside a container is actually working — not just running. A container can have PID 1 alive, listening on a socket, consuming CPU, and still be completely broken: deadlocked, out of database connections, or stuck serving 500 errors.

Before HEALTHCHECK existed, Docker could only tell you whether a container's main process had exited, with no concept of whether that process was doing its job correctly. HEALTHCHECK closes that gap by letting you define a command — typically a curl request, a script, or a lightweight probe — that Docker runs on a schedule inside the container's namespace. Based on that command's exit code, Docker assigns the container a health status you can query with standard tooling.

This works well for long-running services: web servers, APIs, databases, queue consumers. It works far less well for containers that run a job once and exit — which is where a lot of teams get tripped up, and which we'll unpack later.

HEALTHCHECK Syntax and Options

The HEALTHCHECK instruction syntax in a Dockerfile takes one of two forms:

HEALTHCHECK [OPTIONS] CMD command
HEALTHCHECK NONE

The second form disables any healthcheck inherited from a base image. For the first form, command is typically a shell command whose exit code determines status: 0 means healthy, 1 means unhealthy, and 2 is reserved (currently treated as unhealthy).

The available options, with their defaults:

  • --interval (default 30s) — time between health checks
  • --timeout (default 30s) — how long to wait for the check to respond before it counts as a failure
  • --start-period (default 0s) — a grace window after container start during which failures don't count toward the retry limit, useful for slow-booting apps
  • --start-interval (default 5s) — the check interval used specifically during the start period
  • --retries (default 3) — how many consecutive failures are needed before the container is marked unhealthy

A concrete docker healthcheck example using curl:

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

The -f flag makes curl return a non-zero exit code on HTTP error responses, which is what actually triggers the failure — without it, curl might exit 0 even on a 500 response.

The same logic applies in a docker compose healthcheck block:

services:
  api:
    image: my-api:latest
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
      interval: 30s
      timeout: 5s
      start_period: 10s
      retries: 3

Both approaches produce identical behavior; Compose just gives you a cleaner way to configure it per-environment without rebuilding the image.

Health Status States: starting, healthy, unhealthy

Every container with a healthcheck reports one of three states, visible via docker ps (STATUS column) or in full detail via docker inspect:

  • starting — the initial state, active during the start-period window. Failures here don't count against the retry limit.
  • healthy — the check has returned exit code 0 recently enough that the failure count is below the retry threshold.
  • unhealthy — the check has failed consecutively retries times in a row.

To see the full health check log, including the last several attempts and their output, run:

docker inspect --format='{{json .State.Health}}' 

This is the fastest way to answer "why is my docker container unhealthy" — the inspect output shows the actual stdout/stderr and exit code from each failed attempt, rather than making you guess. Common causes include the health endpoint timing out under load, a dependency (database, cache) becoming unreachable, or a start-period set too short for an app with a slow boot sequence.

Why Docker Healthchecks Don't Catch Cron Job Failures

HEALTHCHECK assumes something is continuously running inside the container, and periodically asks it "are you still okay?" That assumption breaks down for a container that runs a scheduled task and then exits — a cron job, a batch import, a nightly report generator.

Docker healthcheck for a cron job container has nothing meaningful to evaluate once the job process has finished. If the container stays alive on some other long-running process (like the cron daemon itself), the healthcheck can report "healthy" indefinitely while the actual task inside silently failed, ran late, or didn't run at all. It only knows the container is up; it has no visibility into whether last night's job exited with an error, timed out halfway through, or executed on schedule at all.

This is also where people conflate Docker's own mechanism with something else entirely: HEALTHCHECK vs Kubernetes liveness probe. They look similar but solve different problems for different orchestrators. Kubernetes ignores the Dockerfile HEALTHCHECK instruction — it has its own liveness, readiness, and startup probes configured directly in the pod spec, controlling restart and traffic-routing behavior independently of anything in the image. If you're running on Kubernetes, the Dockerfile-level check is effectively inert.

None of these mechanisms — Docker's or Kubernetes' — were built to answer "did my scheduled job complete successfully?" That's a job-execution question, not a process-liveness question, and it needs different monitoring. If you're trying to inventory what's actually scheduled across your systems first, listing every cron task across users, systems, Docker, and Kubernetes is a useful starting point before you decide what to monitor.

A container that runs a job and exits needs something that confirms the job itself checked in — a heartbeat or ping-based system, not a process check. That's the layer Cronevra is built for.

Best Practices and What to Pair HEALTHCHECK With

A few docker healthcheck best practices worth following regardless of workload type:

  • Keep the check lightweight — a fast endpoint or simple command, not a full transaction or heavy query. A healthcheck that stresses your own app under load is counterproductive.
  • Set timeout realistically relative to your app's actual response time under normal load, not its best case.
  • Use start-period generously for apps with slow initialization (JVM warmup, migrations, cache priming) so you don't get false unhealthy flags during boot.
  • Tune docker healthcheck retries based on how tolerant you are of transient blips versus how quickly you want orchestration to react — Swarm, for instance, can use health status to decide whether to reroute traffic or restart a service.

For long-running services, HEALTHCHECK plus your orchestrator's own restart logic is often sufficient. For anything scheduled — cron jobs in containers, periodic batch tasks, one-shot ETL runs — pair it with heartbeat-style monitoring designed specifically to track cron jobs in Docker: a lightweight ping sent when the job starts and finishes, so a missing or failed ping raises an alert even though the container itself reports perfectly healthy. Tools built for this pattern (healthchecks.io, Cronitor, and Cronevra all work this way) close the exact gap HEALTHCHECK leaves open.

If your scheduled jobs span multiple containers or nodes, it's also worth understanding where distributed job scheduling tends to fail before you standardize on a monitoring approach.

Cronevra gives you execution history, failure alerts, and heartbeat monitoring purpose-built for scheduled and cron-style jobs — the layer HEALTHCHECK was never designed to cover. Check the pricing page to see which plan fits your team.

Frequently Asked Questions

What does the docker healthcheck command actually do?

It runs a command you define — usually curl, a script, or a binary — inside the container on a set interval, and uses its exit code to assign a health status of starting, healthy, or unhealthy. Docker exposes that status through docker ps and docker inspect so orchestrators and monitoring tools can react to it.

How do I check if a Docker container is healthy?

Run docker ps and look at the STATUS column, which shows (healthy), (unhealthy), or (health: starting) next to the container's uptime. For full details, including recent check output and exit codes, run docker inspect --format='{{json .State.Health}}' .

What's the difference between HEALTHCHECK and a Kubernetes liveness probe?

Dockerfile HEALTHCHECK is a Docker-native instruction that Kubernetes ignores entirely. Kubernetes uses its own liveness, readiness, and startup probes defined in the pod spec, which control restart and traffic routing independently of anything baked into the image.

Why does my container show 'unhealthy' even though the app seems fine?

Usually because the check itself is misconfigured — the timeout is too short for real response times, the start-period doesn't cover slow boot, or the check hits a dependency (database, cache) that's temporarily unreachable. Inspect the health log via docker inspect to see the actual failing command and exit code rather than guessing.

Can I use docker healthcheck to monitor a cron job that runs inside a container?

Not reliably — HEALTHCHECK evaluates whether a process is currently responsive, not whether a scheduled task ran, completed, or ran on time. A container running a cron daemon can report healthy indefinitely while the actual job inside fails silently; you need heartbeat-style ping monitoring alongside it to catch that.

How do I disable a healthcheck inherited from a base image?

Add HEALTHCHECK NONE in your Dockerfile after the FROM line that pulls in the base image. This overrides any HEALTHCHECK instruction baked into the parent image without requiring you to modify or fork it.