Configuring Docker Health Checks for VPS Deployments

Configuring Docker Health Checks for VPS Deployments

What You’ll Need


Table of Contents


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:

  1. Zero-downtime rolling deployments: Prevent reverse proxies from sending traffic to containers before they are fully initialized.
  2. Automated container auto-healing: Automatically restart failed or deadlocked containers on your host server.
  3. 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 of 0 means healthy, while 1 indicates 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 from starting or healthy to unhealthy.
  • 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_on with condition: service_healthy: Replaces naive boot orders. The api_server container 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:

  1. Deploy a Virtual Private Server using Hetzner VPS , Contabo VPS , or DigitalOcean .
  2. Configure custom DNS routing using Namecheap if pointed at reverse proxy containers like Traefik or Nginx Proxy Manager.
  3. Add native HEALTHCHECK instructions into your core application Dockerfile definitions.
  4. Set up docker-compose.yml dependency checks using condition: service_healthy to guarantee correct container boot order.
  5. Deploy an automated monitoring hook or container restarter like willfarrell/autoheal alongside 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
system online