Configuring Docker Health Checks for VPS Deployments

What You’ll Need
- Hetzner VPS or Contabo VPS for hosting
- DigitalOcean as an alternative hosting platform
- Namecheap if you require custom domain routing and SSL endpoints
- n8n Cloud or self-hosted n8n for automated alert workflows
Table of Contents
- Understanding Docker Health Checks on VPS Deployments
- Step 1: Writing Dockerfile HEALTHCHECK Instructions
- Step 2: Orchestrating Health Checks in Docker Compose
- Step 3: Advanced Health Monitoring and Self-Healing Deployments
- Getting Started
Understanding Docker Health Checks on VPS Deployments
When running applications on a bare-metal cloud instance or Virtual Private Server, service availability is your responsibility. Unlike managed PaaS providers that handle pod lifecycles automatically, a single standard Docker container running on a server will stay in a running status even if the process inside hangs, deadlocks, or throws uncaught async exceptions.
Standard process monitoring only tells you if the main PID inside the execution space is alive. It doesn’t tell you if your Node.js event loop is blocked, if your database pool has run out of connections, or if memory exhaustion has made your Web API unresponsive.
By default, Docker lacks visibility into the inner status of your application. Adding native Docker health checks bridges this gap. A health check instructs the Docker engine to periodically execute a command inside the container (or evaluate an HTTP response) to prove the application is functioning properly.
When you configure health checks correctly, you unlock three major capabilities:
- Zero-downtime rolling deployments: Prevent reverse proxies from sending traffic to containers before they are fully initialized.
- Automated container auto-healing: Automatically restart failed or deadlocked containers on your host server.
- Upstream dependency management: Ensure services start in strict order (e.g., waiting for PostgreSQL to accept TCP connections before starting a web worker).
If you are currently migrating workloads to lower overhead infrastructure, check out our guide on How to Reduce Your SaaS Bill with Self-Hosted Alternatives for high-level hosting strategies.
Step 1: Writing Dockerfile HEALTHCHECK Instructions
You can declare a health check directly in your application’s Dockerfile or override it at orchestration time inside your runtime configuration. Defining it in the Dockerfile guarantees that anyone pulling or deploying the image gets built-in monitoring out of the box.
The native instruction format follows this syntax:
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 CMD command
Let’s look at a concrete example using a production Node.js application. First, here is our full application code containing a native HTTP health status endpoint:
const http = require('http');
const PORT = 3000;
const server = http.createServer((req, res) => {
if (req.url === '/healthz') {
const isDatabaseConnected = true;
if (isDatabaseConnected) {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ status: 'ok', uptime: process.uptime() }));
} else {
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ status: 'error', message: 'Database connection failed' }));
}
} else if (req.url === '/') {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('Application is running successfully');
} else {
res.writeHead(404, { 'Content-Type': 'text/plain' });
res.end('Not Found');
}
});
server.listen(PORT, () => {
console.log(`Server listening on port ${PORT}`);
});
To monitor this Node.js app without adding third-party CLI dependencies like curl or wget to our lightweight Alpine base image, we write a small inline Node script directly into the Dockerfile instruction:
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY server.js ./
EXPOSE 3000
HEALTHCHECK --interval=15s --timeout=3s --start-period=5s --retries=3 \
CMD node -e "require('http').get('http://localhost:3000/healthz', (r) => { process.exit(r.statusCode === 200 ? 0 : 1); }).on('error', () => process.exit(1));"
CMD ["node", "server.js"]
If you are using Python with FastAPI or Flask, you might instead install curl in the image or use Python’s built-in urllib package:
from fastapi import FastAPI, Response, status
app = FastAPI()
@app.get("/")
def read_root():
return {"message": "API Operational"}
@app.get("/health")
def health_check(response: Response):
db_ok = True
redis_ok = True
if db_ok and redis_ok:
response.status_code = status.HTTP_200_OK
return {"status": "healthy", "database": "up", "redis": "up"}
else:
response.status_code = status.HTTP_503_SERVICE_UNAVAILABLE
return {"status": "unhealthy", "database": db_ok, "redis": redis_ok}
The accompanying Dockerfile uses Python’s core HTTP handling library:
FROM python:3.11-slim
WORKDIR /app
RUN pip install --no-cache-dir fastapi uvicorn
COPY main.py .
EXPOSE 8000
HEALTHCHECK --interval=20s --timeout=4s --start-period=10s --retries=3 \
CMD python3 -c "import urllib.request; import sys; req = urllib.request.urlopen('http://localhost:8000/health'); sys.exit(0 if req.getcode() == 200 else 1)"
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
💡 Fast-Track Your Project: Don’t want to configure this yourself? I build custom n8n pipelines and bots. Message me with code SYS3-HUGO.
Step 2: Orchestrating Health Checks in Docker Compose
While configuring health checks in the Dockerfile works well for standalone images, defining them in docker-compose.yml gives you central control over your infrastructure stack on a server like Hetzner VPS
or DigitalOcean
.
Here is a full multi-container stack including PostgreSQL, Redis, a Node.js API server, and an automated engine to restart dead containers.
version: '3.8'
services:
postgres:
image: postgres:16-alpine
container_name: production_postgres
environment:
POSTGRES_DB: app_db
POSTGRES_USER: app_user
POSTGRES_PASSWORD: SecurePassword123!
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app_user -d app_db"]
interval: 10s
timeout: 5s
retries: 5
start_period: 10s
restart: always
redis:
image: redis:7-alpine
container_name: production_redis
ports:
- "6379:6379"
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 3
start_period: 5s
restart: always
api_server:
build:
context: .
dockerfile: Dockerfile
container_name: production_api
ports:
- "3000:3000"
environment:
NODE_ENV: production
DATABASE_URL: postgres://app_user:SecurePassword123!@postgres:5432/app_db
REDIS_URL: redis://redis:6379
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
healthcheck:
test: ["CMD-SHELL", "wget --no-verbose --tries=1 --spider http://localhost:3000/healthz || exit 1"]
interval: 15s
timeout: 5s
retries: 3
start_period: 15s
restart: always
autoheal:
image: willfarrell/autoheal:latest
container_name: production_autoheal
environment:
- AUTOHEAL_CONTAINER_LABEL=all
- AUTOHEAL_INTERVAL=10
- AUTOHEAL_START_PERIOD=30
volumes:
- /var/run/docker.sock:/var/run/docker.sock
restart: always
volumes:
postgres_data:
Key Parameters Explained:
test: The actual command executed inside the target container. An exit code of0means healthy, while1indicates unhealthy.interval: How often Docker runs the check command (e.g., every 15 seconds).timeout: Maximum execution window allowed for the check command before Docker considers it a failure.retries: How many consecutive failures must occur before transitioning the status fromstartingorhealthytounhealthy.start_period: Grace window provided upon container startup. Failures during this period will not count toward the maximum retry limit, preventing slow app bootstraps from triggering unnecessary restarts.depends_onwithcondition: service_healthy: Replaces naive boot orders. Theapi_servercontainer will hold off launching until both PostgreSQL and Redis pass their active health checks.
Building network robustness is critical when handling integrations. For more strategies on network-level fault tolerance, see our guide on Designing Resilient Webhook Endpoints with Exponential Backoff .
Step 3: Advanced Health Monitoring and Self-Healing Deployments
By default, when Docker marks a container as unhealthy, it does not automatically restart it. Docker simply changes the container status text in docker ps outputs from (healthy) to (unhealthy).
To make your VPS self-healing, you have two primary approaches: auto-healing wrapper containers or host-level cron script monitoring.
Method 1: Host-Level Bash Monitoring Script
If you do not want to run the willfarrell/autoheal daemon container, you can implement a shell script on your Contabo VPS
host to clean up broken services and post alerts to an external orchestrator.
Save this script as /usr/local/bin/docker-health-monitor.sh:
#!/usr/bin/env bash
set -euo pipefail
WEBHOOK_URL="https://n8n.yourdomain.com/webhook/docker-alert"
UNHEALTHY_CONTAINERS=$(docker ps --filter "health=unhealthy" --format "{{.ID}}:{{.Names}}")
if [ -z "$UNHEALTHY_CONTAINERS" ]; then
echo "[$(date -u +'%Y-%m-%dT%H:%M:%SZ')] All container health checks passing."
exit 0
fi
for ITEM in $UNHEALTHY_CONTAINERS; do
CONTAINER_ID=$(echo "$ITEM" | cut -d':' -f1)
CONTAINER_NAME=$(echo "$ITEM" | cut -d':' -f2)
echo "[$(date -u +'%Y-%m-%dT%H:%M:%SZ')] WARNING: Container $CONTAINER_NAME ($CONTAINER_ID) is UNHEALTHY. Restarting..."
LOG_TAIL=$(docker logs --tail 20 "$CONTAINER_ID" 2>&1 | tr '\n' ' ' | sed 's/"/\\"/g')
docker restart "$CONTAINER_ID"
PAYLOAD=$(cat <<EOF
{
"event": "container_unhealthy_restart",
"container_name": "$CONTAINER_NAME",
"container_id": "$CONTAINER_ID",
"timestamp": "$(date -u +'%Y-%m-%dT%H:%M:%SZ')",
"recent_logs": "$LOG_TAIL"
}
EOF
)
curl -X POST -H "Content-Type: application/json" \
-d "$PAYLOAD" \
"$WEBHOOK_URL" || true
done
Make the script executable and configure cron to evaluate host status every 5 minutes:
chmod +x /usr/local/bin/docker-health-monitor.sh
(crontab -l 2>/dev/null; echo "*/5 * * * * /usr/local/bin/docker-health-monitor.sh >> /var/log/docker-health.log 2>&1") | crontab -
Complex Health Check Scripts inside Containers
For complex services, simple HTTP pings might not be enough. You may want to check disk space, database write access, and queue backlogs in a single script.
Create a dedicated health script named healthcheck.sh inside your application repository:
#!/bin/sh
set -e
HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:3000/healthz)
if [ "$HTTP_STATUS" -ne 200 ]; then
echo "HTTP check failed with status $HTTP_STATUS"
exit 1
fi
DISK_USAGE=$(df -h / | awk 'NR==2 {print $5}' | sed 's/%//')
if [ "$DISK_USAGE" -gt 90 ]; then
echo "Disk usage critically high: $DISK_USAGE%"
exit 1
fi
echo "All systems operational"
exit 0
Inside your Dockerfile:
FROM alpine:3.19
RUN apk add --no-cache curl bash
WORKDIR /app
COPY healthcheck.sh /usr/local/bin/healthcheck.sh
RUN chmod +x /usr/local/bin/healthcheck.sh
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=10s --retries=3 \
CMD /usr/local/bin/healthcheck.sh
CMD ["sh", "-c", "while true; do sleep 3600; done"]
When building automated pipeline tools, orchestrators, and data workers, ensure your underlying data models are flexible enough for container scheduling. To compare platform engines for data handling workloads, see our breakdown on Airbyte vs n8n vs Temporal for API data pipelines .
Getting Started
To implement production-grade container health monitoring on your own infrastructure:
- Deploy a Virtual Private Server using Hetzner VPS , Contabo VPS , or DigitalOcean .
- Configure custom DNS routing using Namecheap if pointed at reverse proxy containers like Traefik or Nginx Proxy Manager.
- Add native
HEALTHCHECKinstructions into your core applicationDockerfiledefinitions. - Set up
docker-compose.ymldependency checks usingcondition: service_healthyto guarantee correct container boot order. - Deploy an automated monitoring hook or container restarter like
willfarrell/autohealalongside n8n Cloud webhook endpoints for instant alert routing to Discord, Slack, or Telegram.
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