sehaj/learn
Docker/03 Running containers

03.07 · build · 14 min

Healthchecks (project step 2)

Running isn't the same as working. Add a healthcheck to the notes API, watch it go healthy and unhealthy, and learn what Docker does (and doesn't do) about it.

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:

Dockerfile
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"]
OptionMeans
--interval=10srun the check every 10 seconds
--timeout=3sa check that takes longer counts as a failure
--start-period=5sfailures during startup don't count against the container
--retries=3three 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

❯terminal
$ 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:

❯terminal
$ 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}}' sick
status=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 healthy before switching traffic to a new container (07.07 Project: deploy to a VPS).
  • Monitoring can alert on it.
Predict first

Your notes API container shows (unhealthy) but the process is still running, and it has --restart always. What will Docker do?

Commit to an answer before you read on.

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 /healthz fails 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).
CheckpointProject step 2: configure and health-check

Bring your notes API up to step 2:

  • Read PORT and MAX_NOTE_LENGTH from the environment, with defaults, and exit 1 on invalid values (03.03 Environment variables and config).
  • Add the HEALTHCHECK above 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:4000 and 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

MidfreqMust-knowConcept

What does a Docker HEALTHCHECK do, and what happens when a container becomes unhealthy?

SeniorfreqGood to knowDesign

Should a service's health endpoint check its database connection?

Report a mistakeEdit this page

Used in