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/venvand an app listening on port8000. - 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¶
- Containers explanation, the background on our distroless base images and why they have no shell.
- Build a multi-stage Dockerfile with our Python base images, the image shape this guide builds on.
- DevSecOps continuous delivery standard, the compliance requirements our base images help projects meet.