Configuring Docker Compose Container Health Checks
What You’ll Need
- Hetzner VPS or Contabo VPS for hosting your production container stack
- DigitalOcean as an alternative cloud infrastructure provider
- n8n Cloud or self-hosted n8n for orchestration and alert notifications
- Namecheap if you need domain names for your public endpoints
Table of Contents
- Understanding Docker Compose Health Checks
- Deep Dive into YAML Configuration Directives
- Advanced Health Checks Dependent Services and Auto Healing
- Monitoring and Debugging Unhealthy Containers
- Getting Started
Understanding Docker Compose Health Checks
A container running PID 1 inside its isolated namespace is not proof that your application is functioning correctly. A Web server might suffer from a deadlock, an API process might hang on an unhandled promise rejection, or a database might exhaust its connection pool. In all these scenarios, Docker marks the container as running because the process has not terminated, yet the application is completely unavailable to users.
Docker health checks solve this problem by executing periodic commands inside the running container to determine actual service viability. When you define a health check in Docker Compose, the Docker daemon runs the specified command inside the container context at fixed intervals. Based on the exit code of that command, Docker updates the container status from starting to healthy or unhealthy.
Here are the four standard exit codes that Docker recognizes for health checks:
0(Success): The container is healthy and ready to serve traffic.1(Unhealthy): The container is not operating correctly.2(Reserved): Do not use this code, as it is reserved for historical Docker internal uses.
When managing infrastructure costs across self-hosted software stacks, maintaining high availability without over-provisioning hardware is critical. If you are analyzing backend server efficiency, check out our breakdown on Temporal vs n8n vs Zapier API Automation Costs to understand how workflow engines impact container resource allocations.
Understanding the lifecycle timing parameters is critical for writing robust health check definitions:
test: The exact command executed inside the container.interval: The time delay between consecutive health check executions.timeout: The maximum allowed execution time for a single health check attempt before marking it as failed.retries: The number of consecutive failures required before Docker marks a container asunhealthy.start_period: A grace period at container boot during which failed health checks do not count toward the maximum retry limit.start_interval: A shorter interval used during thestart_periodphase to verify container readiness faster upon initial boot.
Deep Dive into YAML Configuration Directives
Let us construct a production-ready Docker Compose configuration. Deploying this architecture on a reliable server like a Hetzner VPS gives you complete control over CPU and memory allocations while verifying service status.
Below is a complete docker-compose.yml file containing an Nginx gateway, a Node.js API application, a PostgreSQL database, and a Redis cache. Every service includes explicit health checks configured for its specific runtime characteristics.
version: "3.8"
services:
postgres:
image: postgres:15-alpine
container_name: production_postgres
environment:
POSTGRES_DB: app_db
POSTGRES_USER: db_user
POSTGRES_PASSWORD: SecureDatabasePassword123!
ports:
- "5432:5432"
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U db_user -d app_db"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
start_interval: 2s
restart: always
redis:
image: redis:7-alpine
container_name: production_redis
ports:
- "6379:6379"
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 3
start_period: 5s
restart: always
api:
image: node:18-alpine
container_name: production_api
working_dir: /usr/src/app
command: >
sh -c "node -e '
const http = require(\"http\");
const server = http.createServer((req, res) => {
if (req.url === \"/health\") {
res.writeHead(200, { \"Content-Type\": \"application/json\" });
res.end(JSON.stringify({ status: \"ok\", uptime: process.uptime() }));
} else {
res.writeHead(404);
res.end();
}
});
server.listen(3000, () => console.log(\"API listening on port 3000\"));
'"
ports:
- "3000:3000"
environment:
NODE_ENV: production
DB_HOST: postgres
REDIS_HOST: redis
healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3000/health"]
interval: 15s
timeout: 5s
retries: 3
start_period: 10s
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
restart: always
volumes:
pgdata:
When building high-concurrency database backends, repeatedly creating new database connections for health checks can strain PostgreSQL process limits. You can mitigate this by reviewing our complete guide on Setting Up PostgreSQL Connection Pooling with PgBouncer to keep connection costs minimal during high frequency health checks.
Notice the difference in syntax for the test key across services:
["CMD", "redis-cli", "ping"]: This passes arguments directly to the host process execution layer without invoking a subshell.["CMD-SHELL", "pg_isready -U db_user -d app_db"]: This executes the target command inside the default shell of the container (/bin/sh -c), allowing environment variable evaluation and string concatenation.
💡 Fast-Track Your Project: Don’t want to configure this yourself? I build custom n8n pipelines and bots. Message me with code SYS3-HUGO.
Advanced Health Checks Dependent Services and Auto Healing
One of the greatest features of Docker Compose health checks is service dependency synchronization using condition: service_healthy. Standard depends_on syntax only waits for a dependent container process to enter the running state. It does not wait for the application inside that container to accept network connections. By leveraging health check conditions, dependent containers defer initialization until upstream databases and caches are fully initialized.
However, native Docker Engine does not automatically restart a container merely because its status changes to unhealthy. To implement true automated recovery in standalone Docker Compose environments, we can run a dedicated auto-healing utility like Docker Autoheal directly alongside our application services.
If you expose your health check endpoints over public network interfaces or behind ingress proxies, ensure your endpoints do not become vector targets for denial of service attacks. Read our step-by-step walkthrough on Protecting Self Hosted Endpoints with Rate Limiting to safely restrict access to sensitive service health metrics.
Here is a complete, fully real-world multi-container stack incorporating web proxying, application backends, stateful databases, and auto-healing services running on a DigitalOcean droplet or Contabo VPS.
version: "3.8"
services:
autoheal:
image: willfarrell/autoheal:1.2.0
container_name: service_autoheal
environment:
AUTOHEAL_CONTAINER_HEADER: "all"
AUTOHEAL_INTERVAL: "5"
AUTOHEAL_START_PERIOD: "10"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
restart: always
database:
image: postgres:15-alpine
container_name: stack_database
environment:
POSTGRES_DB: app_production
POSTGRES_USER: admin_user
POSTGRES_PASSWORD: SuperSecretPassword999
healthcheck:
test: ["CMD-SHELL", "pg_isready -U admin_user -d app_production"]
interval: 10s
timeout: 3s
retries: 3
start_period: 15s
restart: always
cache:
image: redis:7-alpine
container_name: stack_cache
healthcheck:
test: ["CMD-SHELL", "redis-cli ping | grep PONG"]
interval: 10s
timeout: 3s
retries: 3
start_period: 5s
restart: always
web_api:
image: nginx:1.25-alpine
container_name: stack_web_api
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
ports:
- "80:80"
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:80/health_check || exit 1"]
interval: 10s
timeout: 5s
retries: 3
start_period: 10s
depends_on:
database:
condition: service_healthy
cache:
condition: service_healthy
labels:
autoheal: "true"
restart: always
Below is the corresponding explicit ./nginx.conf file required by the web_api service to validate server responsiveness without placing heavy load on upstream applications:
server {
listen 80;
server_name localhost;
location /health_check {
access_log off;
add_header Content-Type text/plain;
return 200 'healthy\n';
}
location / {
proxy_pass http://database:5432;
}
}
Monitoring and Debugging Unhealthy Containers
When a health check fails, Docker records the stdout, stderr, and exit code output of the execution script inside the container runtime state. You can inspect this log to troubleshoot why a container transitioned into an unhealthy state.
Run the following command to check the current health status of all managed containers in your Docker Compose directory:
docker compose ps
The output will display the exact status in the STATUS column:
NAME IMAGE COMMAND SERVICE CREATED STATUS PORTS
production_api node:18-alpine "docker-entrypoint.s…" api 5 minutes ago healthy 0.0.0.0:3000->3000/tcp
production_postgres postgres:15-alpine "docker-entrypoint.s…" postgres 5 minutes ago healthy (starting) 0.0.0.0:5432->5432/tcp
production_redis redis:7-alpine "docker-entrypoint.s…" redis 5 minutes ago unhealthy 0.0.0.0:6379->6379/tcp
To view the raw health check logs and diagnostic output for an unhealthy container, execute docker inspect:
docker inspect --format='{{json .State.Health}}' production_redis
To render the JSON output in a clean format using jq, pipe the output as follows:
docker inspect --format='{{json .State.Health}}' production_redis | jq .
The command returns detailed diagnostic trace data formatted like this:
{
"Status": "unhealthy",
"FailingStreak": 4,
"Log": [
{
"Start": "2026-03-30T10:15:30.123456789Z",
"End": "2026-03-30T10:15:30.234567890Z",
"ExitCode": 1,
"Output": "Could not connect to Redis at 127.0.0.1:6379: Connection refused\n"
}
]
}
For complex services requiring multi-step verification (such as validating both network availability and file system write access), move your health logic into a dedicated shell script inside the Docker container image.
Here is a complete, fully written shell script named healthcheck.sh:
#!/bin/sh
set -e
HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:3000/health)
if [ "$HTTP_STATUS" -ne 200 ]; then
echo "HTTP health check failed with status code: $HTTP_STATUS"
exit 1
fi
if [ ! -w /tmp ]; then
echo "Temporary directory /tmp is not writable"
exit 1
fi
echo "All health checks passed successfully"
exit 0
Inside your docker-compose.yml, mount or copy healthcheck.sh into the container, mark it as executable, and invoke it directly in your healthcheck key:
version: "3.8"
services:
worker:
image: alpine:3.18
container_name: background_worker
command: >
sh -c "apk add --no-mode-check curl &&
touch /tmp/worker.log &&
while true; do sleep 3600; done"
volumes:
- ./healthcheck.sh:/usr/local/bin/healthcheck.sh:ro
healthcheck:
test: ["CMD", "/bin/sh", "/usr/local/bin/healthcheck.sh"]
interval: 30s
timeout: 10s
retries: 3
start_period: 5s
restart: always
Getting Started
Deploying fully managed container stacks with health checks ensures that your self-hosted services automatically recover from application deadlocks and transient system failures. To spin up high-performance environments for your automation and deployment pipelines, deploy your Docker Compose infrastructure on a reliable host such as a Hetzner VPS, Contabo VPS, or DigitalOcean droplet. If you configure automated webhooks or public monitoring dashboards, secure your domain records quickly through Namecheap and coordinate custom notifications using n8n Cloud.
Outsource Your Automation
Don’t have time? I build production n8n workflows, WhatsApp bots, and fully automated YouTube Shorts pipelines. Hire me on Fiverr, mention SYS3-HUGO for priority. Or DM at chasebot.online.
Want to automate this yourself?
Start with n8n Cloud (free tier available) or self-host on a Hetzner VPS for full control.
Want this engine running on your own VPS?
This blog publishes itself — daily, unattended, on free API tiers. The full engine, Hugo theme, and setup guide are available as System 3.
Get System 3