Skip to content

How to write a HEALTHCHECK for a distroless image

Our Unified DevOps Platform base images are distroless, so they have no shell and no package manager. That changes how a Docker HEALTHCHECK instruction has to be written. The usual shell-form health check depends on /bin/sh and on tools like curl, neither of which exist in a distroless image, so it fails silently. This guide shows how to write a health check that works, using a tool the image already ships. See the containers explanation for the background on why our base images are distroless.

Prerequisites

  • A multi-stage Dockerfile built on our base images, with a distroless production stage. The build a multi-stage Dockerfile tutorial walks through the shape this guide assumes, including the Python venv at /opt/venv and an app listening on port 8000.
  • A health endpoint your application exposes, for example /healthy.

Use exec form, not shell form

HEALTHCHECK accepts its command in two forms, and the difference matters here.

Shell form takes a bare command and runs it as /bin/sh -c "<command>". Exec form takes a JSON array and runs the executable directly with no shell in between. A distroless image has no /bin/sh, so the shell form cannot run at all. The following looks reasonable but breaks in a distroless image on two counts.

# Wrong: shell form needs /bin/sh, and curl is not present in a distroless image.
HEALTHCHECK CMD curl -f http://localhost:8000/healthy || exit 1

Docker wraps that command in /bin/sh -c, which does not exist, and it invokes curl, which is not installed. The container reports its health as unhealthy or the build behaves unexpectedly, with no obvious cause.

Run a tool the image already has

Write the health check in exec form and run a tool that is already in the image. For our standard Python apps that tool is usually the venv python, which is on PATH at /opt/venv/bin. Python's standard library urllib can make the request, so nothing extra needs to be installed.

HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
    CMD ["python", "-c", "import sys, urllib.request, urllib.error;\ntry:\n  urllib.request.urlopen('http://localhost:8000/healthy'); sys.exit(0)\nexcept Exception:\n  sys.exit(1)"]

The JSON-array form runs python directly, so no shell is involved. python resolves to the venv interpreter already on PATH, and urllib is part of the standard library, so the image gains no new dependencies. The inline script exits 0 when the endpoint returns 200 and 1 otherwise, which is exactly what Docker reads to decide whether the container is healthy.

The same principle applies to other runtimes. Always reach for a health-check mechanism the language runtime already provides rather than an external tool.

Do not add tooling just for a health check

Warning

Do not apt-get install curl, wget, bash, or similar into the production stage, or add them to a runtime-libs stage, solely to support a HEALTHCHECK. Every package added to a distroless image is more CVE surface our vulnerability triage process has to carry, for tooling the workload itself never runs. The point of a distroless image is to ship only what the workload needs, and a health check does not change what the workload needs.

See also