The core idea
Docker knows whether a container's process is running. It has no idea whether the app inside is working: it could be deadlocked, out of database connections, or returning 500 for everything. A healthcheck is a command Docker runs inside the container on a schedule. Exit code 0 means healthy, 1 means unhealthy.
Add one to the notes API
node:22-slim has no curl (02.07 Choosing a base image), so the check asks Node itself to call /healthz:
USER node
EXPOSE 3000
# slim has no curl, so ask Node to call /healthz. Exit 0 = healthy, 1 = unhealthy.
HEALTHCHECK --interval=10s --timeout=3s --start-period=5s --retries=3 \
CMD ["node", "-e", "fetch(`http://127.0.0.1:${process.env.PORT || 3000}/healthz`).then(r => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))"]
CMD ["node", "dist/server.js"]| Option | Means |
|---|---|
--interval=10s | run the check every 10 seconds |
--timeout=3s | a check that takes longer counts as a failure |
--start-period=5s | failures during startup don't count against the container |
--retries=3 | three failures in a row before it's marked unhealthy |
Calling 127.0.0.1 is right here: the check runs inside the container, next to the app. And it reads PORT, so
the check still works if someone reconfigures the port (03.03 Environment variables and config).
Watch it
$ docker build -t notes-api .$ docker run -d --name notes -p 8080:3000 notes-api$ docker ps --filter name=notes --format '{{.Names}} {{.Status}}'notes Up Less than a second (health: starting)$ sleep 12$ docker ps --filter name=notes --format '{{.Names}} {{.Status}}'notes Up 12 seconds (healthy)
docker inspect -f '{{json .State.Health}}' notes shows the status, the current failing streak, and the output and
exit code of the last few checks, which is where to look when one fails.
What unhealthy does (and doesn't) do
Here's the API with a healthcheck that always fails, and a restart policy:
$ docker run -d --name sick --restart unless-stopped \--health-cmd 'exit 1' --health-interval 2s --health-retries 2 notes-api$ docker ps --filter name=sick --format '{{.Names}} {{.Status}}'sick Up 9 seconds (unhealthy)$ docker inspect -f 'status={{.State.Status}} health={{.State.Health.Status}} restarts={{.RestartCount}}' sickstatus=running health=unhealthy restarts=0
Unhealthy, still running, zero restarts. Plain Docker reports health; it doesn't act on it. The value comes from the things that read it:
- Compose can wait for a dependency to be healthy before starting a service (06.03 Startup order and healthchecks).
- Docker Swarm replaces unhealthy tasks; Kubernetes has its own liveness and readiness probes.
- Deploy scripts can wait for
healthybefore switching traffic to a new container (07.07 Project: deploy to a VPS). - Monitoring can alert on it.
Your notes API container shows (unhealthy) but the process is still running, and it has --restart always. What will Docker do?
What a good health endpoint checks
- Cheap and fast. It runs every few seconds; it shouldn't run real queries or allocate much.
- The app's own ability to serve: event loop responsive, server accepting requests.
- Careful with dependencies. If
/healthzfails whenever the database is down, every replica goes unhealthy at once, and an orchestrator may restart all of them, which doesn't fix the database. Many teams split "am I alive?" (liveness) from "can I serve traffic right now?" (readiness).
Bring your notes API up to step 2:
- Read
PORTandMAX_NOTE_LENGTHfrom the environment, with defaults, and exit 1 on invalid values (03.03 Environment variables and config). - Add the
HEALTHCHECKabove to the final stage. - Run it with
--restart unless-stopped --memory 128m --cpus 0.5 --log-opt max-size=10m. - Confirm it reports
(healthy), then run a second copy with-e PORT=4000 -p 8090:4000and confirm that's healthy too.
STUCK? ▸
Stuck on health: starting: look at docker inspect -f '{{json .State.Health}}'. An ECONNREFUSED in the output means the check is calling the wrong port; make sure it reads PORT the same way the app does. Compare with the reference: git diff docker-step-02 -- project/docker/api.
Recap
Interview questions
What does a Docker HEALTHCHECK do, and what happens when a container becomes unhealthy?
Should a service's health endpoint check its database connection?