tutorial

How to Monitor Hetzner Cloud with Vigilmon

Monitor Hetzner Cloud VPS servers, load balancers, object storage, and floating IPs with Vigilmon — CPU/memory/disk utilization via the Hetzner Cloud API, network throughput checks, and alerting for European cloud infrastructure.

Hetzner Cloud offers some of the best price-to-performance ratios in Europe — CCX and CPX VPS instances, dedicated servers, object storage, and load balancers at a fraction of AWS or GCP costs. That's why it's the default cloud for thousands of European startups and DevOps teams. But competitive pricing doesn't come with a built-in observability stack. Hetzner's dashboard shows you basic resource graphs, but it won't page you when a server's CPU pegs at 100%, a floating IP failover silently breaks your load balancer, or your object storage bucket goes unreachable. Vigilmon fills that gap with external HTTP monitors, heartbeat monitors, and alert channels you can set up in minutes.

This tutorial shows you how to monitor Hetzner Cloud servers, load balancers, and object storage with Vigilmon.

What You'll Build

  • A health endpoint deployed on your Hetzner VPS that exposes CPU, memory, and disk metrics
  • Vigilmon HTTP monitors for your servers and load balancer
  • A Hetzner Cloud API poller that surfaces server metrics into a Vigilmon-compatible endpoint
  • Floating IP failover monitoring
  • Alerts for object storage availability

Prerequisites

  • A Hetzner Cloud project with at least one server
  • SSH access to your server and a Hetzner Cloud API token
  • A free account at vigilmon.online

Step 1: Deploy a Health Endpoint on Your Hetzner VPS

The simplest approach is a lightweight HTTP health endpoint that your Vigilmon monitor can poll directly on your server. Use whatever runtime you have available — here's a minimal Node.js/Bun example you can run as a systemd service.

health-server.ts

import { execSync } from "node:child_process";

const PORT = parseInt(process.env.HEALTH_PORT ?? "9090");

function getCpuUsage(): number {
  try {
    // Get 1-second CPU idle percentage using vmstat
    const output = execSync("vmstat 1 2 | tail -1 | awk '{print $15}'")
      .toString()
      .trim();
    return 100 - parseInt(output, 10);
  } catch {
    return -1;
  }
}

function getMemoryUsage(): { used: number; total: number; usedPct: number } {
  try {
    const output = execSync("free -b | grep Mem")
      .toString()
      .trim()
      .split(/\s+/);
    const total = parseInt(output[1], 10);
    const used = parseInt(output[2], 10);
    return { used, total, usedPct: Math.round((used / total) * 100) };
  } catch {
    return { used: 0, total: 0, usedPct: -1 };
  }
}

function getDiskUsage(): { usedPct: number; path: string } {
  try {
    const output = execSync("df -h / | tail -1 | awk '{print $5}'")
      .toString()
      .trim()
      .replace("%", "");
    return { usedPct: parseInt(output, 10), path: "/" };
  } catch {
    return { usedPct: -1, path: "/" };
  }
}

const server = Bun.serve({
  port: PORT,
  fetch(req) {
    const cpu = getCpuUsage();
    const memory = getMemoryUsage();
    const disk = getDiskUsage();

    const ok =
      cpu < 95 &&
      memory.usedPct < 95 &&
      disk.usedPct < 90;

    const body = JSON.stringify({
      status: ok ? "ok" : "degraded",
      checks: {
        cpu: { usedPct: cpu, status: cpu < 95 ? "ok" : "critical" },
        memory: { ...memory, status: memory.usedPct < 95 ? "ok" : "critical" },
        disk: { ...disk, status: disk.usedPct < 90 ? "ok" : "warning" },
      },
    });

    return new Response(body, {
      status: ok ? 200 : 503,
      headers: { "Content-Type": "application/json" },
    });
  },
});

console.log(`Health server running on port ${PORT}`);

Run as a systemd service

# /etc/systemd/system/health-server.service
[Unit]
Description=Vigilmon Health Server
After=network.target

[Service]
ExecStart=/usr/local/bin/bun /opt/health-server/health-server.ts
Restart=always
User=nobody
Environment=HEALTH_PORT=9090

[Install]
WantedBy=multi-user.target
systemctl enable --now health-server
curl http://localhost:9090/

Open port 9090 in your Hetzner Cloud firewall (or use a reverse proxy via nginx/Caddy to expose it over HTTPS).


Step 2: Add Vigilmon HTTP Monitors

Server health monitor

  1. Log in to VigilmonAdd Monitor → HTTP.
  2. URL: https://<your-server-ip-or-domain>:9090/ (or your nginx proxy URL)
  3. Check interval: 60 seconds
  4. Response timeout: 5 seconds
  5. JSON assertion: status = ok
  6. Label: "Hetzner — web-01 health"

Add one monitor per server. Hetzner CCX instances are priced for multi-server setups — monitor each one independently so you can identify the failing node.

Load balancer monitor

  1. Add Monitor → HTTP.
  2. URL: https://<your-load-balancer-public-ip>/ (or your domain)
  3. Expected status: 200
  4. Label: "Hetzner — load balancer"

Step 3: Poll Hetzner Cloud API for Server Metrics

Hetzner Cloud exposes server CPU, disk I/O, and network metrics via its API. You can build a poller that fetches these and exposes them as a Vigilmon-compatible health endpoint — useful if you can't or don't want to install an agent on every server.

hetzner-metrics-poller.ts

const HCLOUD_TOKEN = process.env.HCLOUD_TOKEN!;
const SERVER_ID = process.env.HCLOUD_SERVER_ID!;

interface HetznerMetrics {
  time_series: {
    "cpu.0"?: { values: [number, string][] };
    "disk.0.bandwidth.read"?: { values: [number, string][] };
    "network.0.bandwidth.in"?: { values: [number, string][] };
  };
}

export async function fetchServerMetrics(): Promise<{
  cpuPct: number;
  networkInMbps: number;
}> {
  const end = new Date();
  const start = new Date(end.getTime() - 5 * 60 * 1000); // last 5 minutes

  const url = new URL(
    `https://api.hetzner.cloud/v1/servers/${SERVER_ID}/metrics`
  );
  url.searchParams.set("type", "cpu,network");
  url.searchParams.set("start", start.toISOString());
  url.searchParams.set("end", end.toISOString());
  url.searchParams.set("step", "60");

  const resp = await fetch(url.toString(), {
    headers: { Authorization: `Bearer ${HCLOUD_TOKEN}` },
    signal: AbortSignal.timeout(5000),
  });

  if (!resp.ok) {
    throw new Error(`Hetzner API error: ${resp.status}`);
  }

  const data: { metrics: HetznerMetrics } = await resp.json();
  const cpuValues = data.metrics.time_series["cpu.0"]?.values ?? [];
  const netInValues =
    data.metrics.time_series["network.0.bandwidth.in"]?.values ?? [];

  const avgCpu =
    cpuValues.length > 0
      ? cpuValues.reduce((sum, [, v]) => sum + parseFloat(v), 0) /
        cpuValues.length
      : 0;

  const avgNetIn =
    netInValues.length > 0
      ? netInValues.reduce((sum, [, v]) => sum + parseFloat(v), 0) /
        netInValues.length /
        1_000_000 // bytes/s → Mbps
      : 0;

  return { cpuPct: Math.round(avgCpu), networkInMbps: Math.round(avgNetIn) };
}

Wrap this in a health endpoint that Vigilmon can poll, or call it from a scheduled job that posts results to a Vigilmon webhook.


Step 4: Monitor Floating IP Failover

Hetzner floating IPs let you reassign a public IP between servers for failover. If the failover fails or takes too long, your IP points at a dead server. Monitor the floating IP directly so Vigilmon catches the gap.

Add an HTTP monitor for each floating IP:

  1. Add Monitor → HTTP.
  2. URL: http://<floating-ip>/health (your health endpoint)
  3. Check interval: 30 seconds (shorter interval for failover detection)
  4. Response timeout: 5 seconds
  5. Label: "Hetzner — floating IP failover"

When a failover completes successfully, the monitor recovers automatically. If the monitor stays down more than 2 checks, Vigilmon fires an alert before your users do.


Step 5: Monitor Hetzner Object Storage

Hetzner Object Storage is S3-compatible. You can monitor bucket availability with a simple HEAD request to a known object.

Create a small "canary" object in your bucket:

# Upload a canary file using the AWS CLI (compatible with Hetzner Object Storage)
aws s3 cp /dev/null s3://<your-bucket>/health-canary \
  --endpoint-url https://fsn1.your-objectstorage.com

Then add a Vigilmon HTTP monitor:

  1. Add Monitor → HTTP.
  2. URL: https://fsn1.your-objectstorage.com/<your-bucket>/health-canary
  3. Method: HEAD
  4. Expected status: 200
  5. Label: "Hetzner — object storage"

This checks that your S3-compatible endpoint is reachable and returning objects — covering network, bucket policy, and storage availability.


Step 6: Embed the Status Page

Go to Status Pages → Create in Vigilmon, add your Hetzner monitors, and embed the badge in your README:

[![Uptime](https://vigilmon.online/badge/<monitor-id>.svg)](https://vigilmon.online/status/<status-page-slug>)

Or embed the live widget in your internal ops dashboard:

<script
  src="https://vigilmon.online/embed.js"
  data-page="<status-page-slug>"
  data-theme="dark"
  async
></script>

Key Metrics to Monitor

| Metric | Source | Vigilmon feature | |---|---|---| | Server availability | Health endpoint on each VPS | HTTP monitor, 60 s interval | | CPU utilization | Health endpoint or Hetzner API | JSON assertion on cpuPct < 95 | | Memory utilization | Health endpoint | JSON assertion on usedPct < 95 | | Disk utilization | Health endpoint | JSON assertion on usedPct < 90 | | Network throughput | Hetzner API poller | JSON assertion on networkInMbps | | Load balancer availability | HTTP monitor on LB IP | HTTP monitor, expected 200 | | Floating IP failover | HTTP monitor on floating IP | 30 s interval HTTP monitor | | Object storage | HEAD request to canary object | HTTP monitor, expected 200 |


Alerting Recommendations

  • Slack webhook — post to #infrastructure when any server monitor degrades
  • Email — on-call address for disk-full or memory-critical alerts
  • PagerDuty — for floating IP failover failures and load balancer outages

Hetzner's hardware is reliable, but software-layer failures (OOM kills, filesystem full, misconfigured firewalls) are your responsibility to catch. Set Vigilmon re-notification to every 5 minutes while a monitor is down.


Hetzner Cloud runs your servers. Vigilmon tells you whether they're actually serving your users.

Start monitoring your Hetzner Cloud infrastructure today — register free at vigilmon.online.

Monitor your app with Vigilmon

Free plan — 5 monitors, no credit card required. Up and running in 60 seconds.

Start free →