A running container is not the same as a working app. The process can be up while the app is stuck, still loading or unable to reach its database. A health check is a command Docker runs inside the container at intervals to find out. This guide shows how to write health checks, read the result and use it to start services in the right order.
Before you start
- A Linux VPS with Docker Engine and the Compose plugin. See Install Docker and Docker Compose.
- An app with something cheap to test, ideally an HTTP endpoint such as
/healththat returns200when the app and its dependencies are fine. - Knowledge of which tools exist inside the image. A check can only use commands the image contains, such as
curl,wgetor the app's own CLI.
How health checks work
Docker runs the check command inside the container on a schedule:
- Exit code
0means healthy. - Exit code
1means unhealthy.
The container's status moves from starting to healthy, or to unhealthy after a set number of failures in a row. You see it in docker ps:
CONTAINER ID IMAGE STATUS
3f2c1ab9d0e4 myapp:1.0 Up 2 minutes (healthy)Four settings control the timing:
- interval: time between checks (default 30 seconds).
- timeout: how long one check may take before it counts as failed (default 30 seconds).
- retries: failures in a row before the status becomes
unhealthy(default 3). - start_period: a grace period after start during which failures do not count, for apps that load slowly.
Add a health check in a Dockerfile
For an image you build yourself:
FROM node:22-alpine
WORKDIR /app
COPY . .
HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \
CMD wget -q --spider http://127.0.0.1:3000/health || exit 1
CMD ["node", "server.js"]Alpine images include BusyBox wget. Debian-based images often have neither curl nor wget, so install one in the Dockerfile or use a small script in the app's own language.
Add or override a health check in Compose
Compose lets you add checks to third-party images without rebuilding:
services:
app:
image: myapp:1.0
healthcheck:
test: ["CMD", "curl", "-fsS", "http://127.0.0.1:3000/health"]
interval: 30s
timeout: 5s
retries: 3
start_period: 20scurl -f returns a non-zero exit code for HTTP errors such as 500, which is what you want.
Ready-made checks for common databases:
db:
image: postgres:16
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5
redis:
image: redis:7
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 5Use CMD-SHELL when the test needs shell features such as || or variables. To turn off a check that an image defines, set healthcheck: { disable: true }.
Start services in order with service_healthy
By default depends_on only waits for a container to start, not to be ready. A web app often crashes because its database is still initialising. Make it wait for the health check instead:
services:
app:
image: myapp:1.0
depends_on:
db:
condition: service_healthy
db:
image: postgres:16
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
retries: 5docker compose up -d now starts db, waits until it reports healthy, then starts app. Add --wait to make the command itself wait until every service is healthy, which is useful in deploy scripts:
docker compose up -d --waitHealth checks and restarts
Plain Docker Engine does not restart a container just because it is unhealthy. Restart policies react only when the main process exits. Health status is used by:
depends_onwithcondition: service_healthy.docker compose up --wait.- Reverse proxies such as Traefik, which can skip unhealthy containers.
- Docker Swarm, which does replace unhealthy tasks.
- Monitoring tools that read the status.
If you need automatic recovery, make the app exit when it detects it is broken so its restart policy kicks in, or alert on unhealthy containers with an external monitor such as Uptime Kuma.
Read the health status
docker inspect --format '{{json .State.Health}}' appThe output includes the current status, the failing streak and the last five check results with their output. That output is usually the fastest way to see why a check fails.
Check it works
Watch the status change after a start:
docker compose up -d
docker compose psThe STATUS column should show (health: starting) and then (healthy) once the start period and first checks pass. To test the unhealthy path, stop the app's dependency, such as the database, and watch the app's status turn (unhealthy) after the configured retries.
Troubleshooting
- Status stays
startingthen turnsunhealthy: run the test command yourself withdocker exec app curl -fsS http://127.0.0.1:3000/healthand read the error. exec: "curl": executable file not found: the image lacks curl. Usewget, a language-native check or install curl in your Dockerfile.- Check fails on
localhostbut the app works: the app listens only on IPv4 whilelocalhostresolves to IPv6 first. Use127.0.0.1. - Checks pile up and use CPU: the interval is too short or the check is expensive. Keep checks light and every 10–30 seconds.
Next steps
- Keep containers up after crashes: Docker restart policies.
- Use health checks for safer releases: Near zero-downtime deploys with Docker Compose.
