Skip to content

Instantly share code, notes, and snippets.

@supermarsx
Last active August 26, 2025 15:59
Show Gist options
  • Save supermarsx/d4c8ae4b11134247ac3f0acf75258290 to your computer and use it in GitHub Desktop.
Save supermarsx/d4c8ae4b11134247ac3f0acf75258290 to your computer and use it in GitHub Desktop.
Docker Compose Healthchecks for containers

Docker Compose Healthchecks

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_on so stacks start in the right order

TL;DR — Quick Start

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.

  • CMD vs CMD-SHELL:

    • CMD runs the command directly without a shell (safer, no shell parsing).
    • CMD-SHELL runs 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> | jq

Wire service startup to health state:

services:
  app:
    depends_on:
      db:
        condition: service_healthy

Compose v2 supports condition: service_healthy in depends_on.


Ready‑to‑Paste Healthcheck Catalog

Below are tested patterns you can drop into your docker-compose.yml. Adjust ports, credentials, and endpoints to your setup.

MariaDB (official image script)

# mariadb
healthcheck:
  test: healthcheck.sh --connect --innodb_initialized
  interval: 30s
  timeout: 10s
  retries: 3
  start_period: 10s
# ENV: MARIADB_ROOT_PASSWORD: password...

MariaDB (simple SQL probe)

# 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 (port 9000)

# 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 (port 80)

# generic nginx
healthcheck:
  test: curl -sf http://127.0.0.1/
  interval: 30s
  timeout: 10s
  retries: 3
  start_period: 15s

SQLPad

# 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

Nginx Manager (port 81)

# generic nginx manager
healthcheck:
  test: curl -sf http://127.0.0.1:81/
  interval: 30s
  timeout: 10s
  retries: 3
  start_period: 15s

Portainer (port 9000)

# portainer
healthcheck:
  test: wget --no-verbose --tries=1 --spider http://localhost:9000/ || exit 1
  interval: 30s
  timeout: 10s
  retries: 3
  start_period: 10s

Gitea (port 80)

# gitea   
healthcheck:
  test: ["CMD", "curl", "-sf", "http://127.0.0.1:80/"]
  interval: 30s
  timeout: 10s
  retries: 3

Docker Registry (port 5000)

# 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 (port 80)

# 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

# redis
healthcheck:
  test: ["CMD-SHELL", "redis-cli ping | grep PONG || exit 1"]
  interval: 10s
  timeout: 5s
  retries: 3
  start_period: 10s

Drone Runner (port 3000)

# 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 Server

# drone
healthcheck:
  test: ["CMD-SHELL", "wget --quiet --spider http://127.0.0.1/healthz || exit 1"]
  interval: 30s
  timeout: 10s
  retries: 3
  start_period: 30s

Using Health with depends_on

Ensure 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_healthy

This avoids race conditions (e.g., app crashing because DB isn’t ready yet).


Patterns & Best Practices

  • Pick the lightest correct probe. TCP port checks (fsockopen, nc -z, or curl -sf) are fast. Prefer application‑level probes (HTTP 200, SQL SELECT 1) when possible.
  • Use CMD for simple commands and CMD-SHELL when you need shell features (pipes, ||, env interpolation quirks).
  • Graceful startup: databases often need longer start_period.
  • Keep credentials out of test when possible. If you must, prefer using env vars and least‑privileged users.
  • Alpine vs Debian images: curl/wget/bash may 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 HEALTHCHECK vs Compose healthcheck

  • Dockerfile HEALTHCHECK bakes the probe into the image — reusable across stacks.
  • Compose healthcheck configures 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.


Troubleshooting

  • 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.). Increase start_period if 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.


Reusable YAML Anchors (DRY)

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

Reference Matrix

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

Security Notes

  • 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.

Example: End‑to‑End Compose Snippet

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: 10s

FAQ

Q: 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.).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment