Healthchecks let Docker verify that a container is actually ready and responding (not just “running”). This README gives you:
- A 2‑minute intro to how healthchecks work in Compose
- Ready‑to‑paste examples for common services (MariaDB, Nextcloud FPM, Nginx, Portainer, Gitea, Registry, Redis, Drone…)
- Patterns, best practices, and troubleshooting tips
- How to wire healthchecks into
depends_onso stacks start in the right order
Add a healthcheck block under a service in your docker-compose.yml:
services:
myservice:
image: myimage:latest
healthcheck:
test: ["CMD", "curl", "-sf", "http://127.0.0.1/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 15s-
Exit code matters:
0= healthy, non‑zero = unhealthy. -
CMDvsCMD-SHELL:CMDruns the command directly without a shell (safer, no shell parsing).CMD-SHELLruns via/bin/sh -c(useful for pipes/|| exit 1).
-
start_period: grace period before counting failures (great while DBs initialize).
Check status:
docker compose ps
# or
docker inspect --format='{{json .State.Health}}' <container_id> | jqWire service startup to health state:
services:
app:
depends_on:
db:
condition: service_healthyCompose v2 supports
condition: service_healthyindepends_on.
Below are tested patterns you can drop into your docker-compose.yml. Adjust ports, credentials, and endpoints to your setup.
# mariadb
healthcheck:
test: healthcheck.sh --connect --innodb_initialized
interval: 30s
timeout: 10s
retries: 3
start_period: 10s
# ENV: MARIADB_ROOT_PASSWORD: password...# mariadb alternative
healthcheck:
test: ["CMD-SHELL", "mysql -u root -pexample -e 'SELECT 1' || exit 1"]
interval: 30s
timeout: 10s
retries: 3
start_period: 10s# nextcloud fpm
healthcheck:
test: php -r 'if (@fsockopen("127.0.0.1", 9000)) print("OK"); else print("ERROR");';
interval: 20s
timeout: 10s
retries: 3
start_period: 10s# generic nginx
healthcheck:
test: curl -sf http://127.0.0.1/
interval: 30s
timeout: 10s
retries: 3
start_period: 15s# sqlpad
healthcheck:
test: ["CMD-SHELL", "wget --quiet --tries=1 --spider http://127.0.0.1/ || exit 1"]
interval: 30s
timeout: 10s
retries: 3
start_period: 15s# generic nginx manager
healthcheck:
test: curl -sf http://127.0.0.1:81/
interval: 30s
timeout: 10s
retries: 3
start_period: 15s# portainer
healthcheck:
test: wget --no-verbose --tries=1 --spider http://localhost:9000/ || exit 1
interval: 30s
timeout: 10s
retries: 3
start_period: 10s# gitea
healthcheck:
test: ["CMD", "curl", "-sf", "http://127.0.0.1:80/"]
interval: 30s
timeout: 10s
retries: 3# registry
healthcheck:
test: ["CMD-SHELL", "wget --quiet --spider http://127.0.0.1:5000/ || exit 1"]
interval: 30s
timeout: 10s
retries: 3
start_period: 10s# registry ui
healthcheck:
test: ["CMD-SHELL", "wget --quiet --spider http://127.0.0.1:80/ || exit 1"]
interval: 30s
timeout: 10s
retries: 3
start_period: 10s# redis
healthcheck:
test: ["CMD-SHELL", "redis-cli ping | grep PONG || exit 1"]
interval: 10s
timeout: 5s
retries: 3
start_period: 10s# drone runner
healthcheck:
test: ["CMD-SHELL", "wget --quiet --spider http://127.0.0.1:3000/healthz || exit 1"]
interval: 30s
timeout: 10s
retries: 3
start_period: 30s# drone
healthcheck:
test: ["CMD-SHELL", "wget --quiet --spider http://127.0.0.1/healthz || exit 1"]
interval: 30s
timeout: 10s
retries: 3
start_period: 30sEnsure services don’t start until dependencies are actually ready:
services:
db:
image: mariadb:11
# (include one of the healthchecks above)
app:
image: myorg/myapp:latest
depends_on:
db:
condition: service_healthyThis avoids race conditions (e.g., app crashing because DB isn’t ready yet).
- Pick the lightest correct probe. TCP port checks (
fsockopen,nc -z, orcurl -sf) are fast. Prefer application‑level probes (HTTP 200, SQLSELECT 1) when possible. - Use
CMDfor simple commands andCMD-SHELLwhen you need shell features (pipes,||, env interpolation quirks). - Graceful startup: databases often need longer
start_period. - Keep credentials out of
testwhen possible. If you must, prefer using env vars and least‑privileged users. - Alpine vs Debian images:
curl/wget/bashmay not be present. Install via Dockerfile if needed. - Avoid external dependencies. Probe loopback (
127.0.0.1,localhost) inside the container. - Consistent intervals/timeouts. Start with
interval: 30s,timeout: 10s,retries: 3; tune as needed. - Don’t confuse container “running” with “ready.” Healthchecks are the bridge.
- Dockerfile
HEALTHCHECKbakes the probe into the image — reusable across stacks. - Compose
healthcheckconfigures the probe per deployment — flexible per environment.
It’s fine to combine: an image may define a default HEALTHCHECK, which you override or extend in Compose if needed.
-
See recent health logs:
docker inspect <container> --format='{{json .State.Health}}' | jq
-
Command not found? Your base image may lack
curl/wget/redis-cli. Add them in the Dockerfile or switch to a tool that exists (e.g.,nc, raw TCP check, or an app‑native CLI). -
False positives/negatives: Use app‑level endpoints (e.g.,
/healthz) and ensure the probe covers critical dependencies (DB, cache, migrations done, etc.). Increasestart_periodif initialization is slow. -
Credentials failing for DB checks: Confirm env vars are available to the healthcheck process. Some entrypoints set users later; use the official health scripts when available.
x-health:
curl80: &curl80
test: ["CMD", "curl", "-sf", "http://127.0.0.1:80/"]
interval: 30s
timeout: 10s
retries: 3
start_period: 15s
services:
gitea:
image: gitea/gitea:latest
healthcheck: *curl80| Service | Probe Type | Endpoint/Command | |
|---|---|---|---|
| MariaDB | Script (official) | healthcheck.sh --connect --innodb_initialized |
|
| MariaDB (alt) | SQL | mysql -u root -p****** -e 'SELECT 1' |
|
| Nextcloud FPM | TCP port | PHP fsockopen(127.0.0.1:9000) |
|
| Nginx | HTTP 200 | curl -sf http://127.0.0.1/ |
|
| SQLPad | HTTP 200 | wget --spider http://127.0.0.1/ |
|
| Nginx Manager | HTTP 200 | curl -sf http://127.0.0.1:81/ |
|
| Portainer | HTTP 200 | wget --spider http://localhost:9000/ |
|
| Gitea | HTTP 200 | curl -sf http://127.0.0.1:80/ |
|
| Registry | HTTP 200 | wget --spider http://127.0.0.1:5000/ |
|
| Registry UI | HTTP 200 | wget --spider http://127.0.0.1:80/ |
|
| Redis | CLI ping | `redis-cli ping | grep PONG` |
| Drone Runner | Health endpoint | wget --spider http://127.0.0.1:3000/healthz |
|
| Drone | Health endpoint | wget --spider http://127.0.0.1/healthz |
- Avoid embedding secrets directly in
test. Prefer env vars or dedicated least‑privileged users. - Probes should only touch internal endpoints.
- If you expose
/healthz, ensure it doesn’t leak sensitive info.
version: "3.9"
services:
db:
image: mariadb:11
environment:
MARIADB_ROOT_PASSWORD: example
healthcheck:
test: ["CMD-SHELL", "mysql -uroot -pexample -e 'SELECT 1' || exit 1"]
interval: 30s
timeout: 10s
retries: 3
start_period: 20s
app:
image: myorg/myapp:latest
depends_on:
db:
condition: service_healthy
healthcheck:
test: ["CMD", "curl", "-sf", "http://127.0.0.1/healthz"]
interval: 30s
timeout: 10s
retries: 3
start_period: 10sQ: Can I run multiple commands in test?
Use CMD-SHELL with && / || chaining, or wrap logic in a small script inside the image.
Q: How often should I probe? Default to every 30s with 3 retries. Busy services or flaky networks may warrant longer intervals/timeouts.
Q: Do healthchecks restart containers?
No. Health state is informational. Combine with a process manager or add a sidecar/watcher if you want automatic restarts on unhealthy (or rely on your orchestrator/k8s in larger deployments).
Happy shipping! Drop these blocks into your Compose files and tune per service behavior. If you want, we can add more service recipes (Postgres, RabbitMQ, MinIO, Keycloak, etc.).