VPNSmith
self-host-vpnINFO

Monitor your VPS VPN with Prometheus + Grafana (2026)

Full monitoring stack: node_exporter, wireguard_exporter, Grafana dashboards, uptime/throughput alerts, Discord webhook integration. 30-min setup.

By Eric Gerard · Fondateur · VPNSmith — Spécialiste self-host VPN & VPS GDPR7 min readPhoto via Unsplash

Your WireGuard tunnel runs. But you wonder: how many Mbps does it pull at peak? Which peers connect when? Does the Contabo VPS actually hit its advertised 200 Mbps? You could open a terminal and tail -f journalctl every time, or install a Prometheus + Grafana stack in 30 minutes that gives you visual answers, alerts on incidents, and keeps history.

This guide implements the setup we run ourselves on Contabo VPS S for 14 months. node_exporter for the system, wireguard_exporter for peers, Prometheus for storage, Grafana for visualization, Alertmanager + Discord webhook for alerts.

Architecture

Everything runs on the same Contabo VPS. Three systemd processes:

  • node_exporter (port 9100): system metrics (CPU, RAM, disk, network)
  • wireguard_exporter (port 9586): WireGuard peer metrics (handshake, bytes_in/out, last_seen)
  • prometheus (port 9090): scrapes exporters every 15s, retains 30 days
  • grafana (port 3000): dashboards
  • alertmanager (port 9093): alert rules + Discord webhook

Cost: ~250 MB RAM, ~3% CPU steady on a 4 vCPU VPS. No perceptible impact on the tunnel.

Step 1 — Install node_exporter

# On the VPS, with sudo
sudo useradd --no-create-home --shell /usr/sbin/nologin node_exporter

cd /tmp
wget https://github.com/prometheus/node_exporter/releases/download/v1.8.2/node_exporter-1.8.2.linux-amd64.tar.gz
tar xzf node_exporter-1.8.2.linux-amd64.tar.gz
sudo cp node_exporter-1.8.2.linux-amd64/node_exporter /usr/local/bin/
sudo chown node_exporter:node_exporter /usr/local/bin/node_exporter

systemd unit /etc/systemd/system/node_exporter.service:

[Unit]
Description=Node Exporter
After=network.target

[Service]
User=node_exporter
Group=node_exporter
Type=simple
ExecStart=/usr/local/bin/node_exporter --web.listen-address=127.0.0.1:9100

[Install]
WantedBy=multi-user.target

Important: we bind on 127.0.0.1 (not 0.0.0.0) — no public exposure. Prometheus accesses via localhost.

sudo systemctl daemon-reload
sudo systemctl enable --now node_exporter
curl -s http://127.0.0.1:9100/metrics | head -20
# Should return metrics like "node_cpu_seconds_total"

Step 2 — Install prometheus-wireguard-exporter

The exporter we use: github.com/MindFlavor/prometheus_wireguard_exporter (Rust, ~3 MB binary, scrapes wg show output).

cd /tmp
wget https://github.com/MindFlavor/prometheus_wireguard_exporter/releases/download/3.6.6/prometheus_wireguard_exporter_3.6.6_linux_amd64.tar.gz
tar xzf prometheus_wireguard_exporter_3.6.6_linux_amd64.tar.gz
sudo cp prometheus_wireguard_exporter /usr/local/bin/
sudo chmod +x /usr/local/bin/prometheus_wireguard_exporter

systemd unit /etc/systemd/system/wireguard_exporter.service:

[Unit]
Description=WireGuard Prometheus Exporter
After=network.target wg-quick@wg0.service
Requires=wg-quick@wg0.service

[Service]
User=root
ExecStart=/usr/local/bin/prometheus_wireguard_exporter -a 127.0.0.1 -p 9586 -n /etc/wireguard/wg0.conf
Restart=on-failure

[Install]
WantedBy=multi-user.target

The -n /etc/wireguard/wg0.conf flag lets the exporter use peer names (commented in the .conf) as Prometheus labels. More readable in Grafana than bare pubkeys.

sudo systemctl daemon-reload
sudo systemctl enable --now wireguard_exporter
curl -s http://127.0.0.1:9586/metrics | grep wireguard_
# Should return wireguard_sent_bytes_total, wireguard_received_bytes_total, etc.

Step 3 — Install Prometheus

sudo useradd --no-create-home --shell /usr/sbin/nologin prometheus
sudo mkdir -p /etc/prometheus /var/lib/prometheus
sudo chown prometheus:prometheus /etc/prometheus /var/lib/prometheus

cd /tmp
wget https://github.com/prometheus/prometheus/releases/download/v2.55.1/prometheus-2.55.1.linux-amd64.tar.gz
tar xzf prometheus-2.55.1.linux-amd64.tar.gz
sudo cp prometheus-2.55.1.linux-amd64/prometheus /usr/local/bin/
sudo cp prometheus-2.55.1.linux-amd64/promtool /usr/local/bin/
sudo chown prometheus:prometheus /usr/local/bin/prometheus /usr/local/bin/promtool

Config /etc/prometheus/prometheus.yml:

global:
  scrape_interval: 15s
  evaluation_interval: 15s

alerting:
  alertmanagers:
    - static_configs:
        - targets:
            - 127.0.0.1:9093

rule_files:
  - "alert_rules.yml"

scrape_configs:
  - job_name: prometheus
    static_configs:
      - targets: [127.0.0.1:9090]

  - job_name: node
    static_configs:
      - targets: [127.0.0.1:9100]
        labels:
          instance: vps-contabo-nuremberg

  - job_name: wireguard
    static_configs:
      - targets: [127.0.0.1:9586]
        labels:
          instance: vps-contabo-nuremberg

Alert rules /etc/prometheus/alert_rules.yml:

groups:
  - name: vpn_alerts
    interval: 30s
    rules:
      - alert: HighCPU
        expr: 100 - (avg by (instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100) > 80
        for: 5m
        annotations:
          summary: "CPU > 80% on {{ $labels.instance }}"

      - alert: WireGuardPeerDown
        expr: time() - wireguard_latest_handshake_seconds > 600
        for: 5m
        annotations:
          summary: "WG peer {{ $labels.peer }} hasn't handshaken in >10min"

      - alert: HighBandwidth
        expr: rate(node_network_transmit_bytes_total{device="eth0"}[5m]) * 8 > 180000000
        for: 10m
        annotations:
          summary: "Bandwidth > 180 Mbps on eth0 — approaching Contabo limit"

      - alert: DiskAlmostFull
        expr: (node_filesystem_avail_bytes{mountpoint="/"} / node_filesystem_size_bytes{mountpoint="/"}) * 100 < 15
        for: 10m
        annotations:
          summary: "Disk < 15% free on {{ $labels.instance }}"

systemd unit /etc/systemd/system/prometheus.service:

[Unit]
Description=Prometheus
After=network.target

[Service]
User=prometheus
Group=prometheus
Type=simple
ExecStart=/usr/local/bin/prometheus \
  --config.file /etc/prometheus/prometheus.yml \
  --storage.tsdb.path /var/lib/prometheus/ \
  --storage.tsdb.retention.time=30d \
  --web.listen-address=127.0.0.1:9090

[Install]
WantedBy=multi-user.target
sudo chown prometheus:prometheus /etc/prometheus/prometheus.yml /etc/prometheus/alert_rules.yml
sudo systemctl daemon-reload
sudo systemctl enable --now prometheus
sudo systemctl status prometheus

Step 4 — Install Grafana

sudo apt install -y apt-transport-https software-properties-common
sudo mkdir -p /etc/apt/keyrings/
wget -q -O - https://apt.grafana.com/gpg.key | sudo gpg --dearmor -o /etc/apt/keyrings/grafana.gpg
echo "deb [signed-by=/etc/apt/keyrings/grafana.gpg] https://apt.grafana.com stable main" | sudo tee /etc/apt/sources.list.d/grafana.list
sudo apt update
sudo apt install -y grafana

Edit /etc/grafana/grafana.ini to bind localhost only:

[server]
http_addr = 127.0.0.1
http_port = 3000
sudo systemctl enable --now grafana-server

Step 5 — Access Grafana securely

Instead of opening port 3000 publicly, SSH tunnel from your laptop:

ssh -L 3000:127.0.0.1:3000 eric@vpn.example.com

Then open http://127.0.0.1:3000 in your local browser. Login admin / admin (change immediately).

Alternative: access via the WireGuard tunnel. If your VPS is 10.66.66.1 on the VPN side, hit http://10.66.66.1:3000 from any connected peer. No port exposed to public Internet.

Step 6 — Configure the Prometheus datasource

In Grafana:

  1. Settings → Data Sources → Add data source → Prometheus
  2. URL: http://127.0.0.1:9090
  3. Save & Test → "Data source is working"

Step 7 — Import dashboards

Ready-made dashboards:

  • Node Exporter Full: ID 1860 on grafana.com — full system metrics
  • WireGuard: ID 12557 or newer — peers, bandwidth, last_seen

In Grafana: Dashboards → Import → paste the ID → pick the Prometheus datasource → Import.

For a custom VPS-VPN dashboard, we published our JSON on GitHub (link in the VPNSmith repo). It contains:

  • Overview: RAM/CPU/disk/uptime
  • eth0 bandwidth (in/out, 1h/24h/7d)
  • WireGuard peers: name, last handshake, exchanged bytes
  • Top peers by usage
  • Active alerts

Step 8 — Discord alerts via webhook

Install Alertmanager:

cd /tmp
wget https://github.com/prometheus/alertmanager/releases/download/v0.27.0/alertmanager-0.27.0.linux-amd64.tar.gz
tar xzf alertmanager-0.27.0.linux-amd64.tar.gz
sudo cp alertmanager-0.27.0.linux-amd64/alertmanager /usr/local/bin/
sudo cp alertmanager-0.27.0.linux-amd64/amtool /usr/local/bin/
sudo useradd --no-create-home --shell /usr/sbin/nologin alertmanager
sudo mkdir -p /etc/alertmanager /var/lib/alertmanager
sudo chown alertmanager:alertmanager /etc/alertmanager /var/lib/alertmanager

Config /etc/alertmanager/alertmanager.yml:

global:
  resolve_timeout: 5m

route:
  receiver: discord

receivers:
  - name: discord
    webhook_configs:
      - url: 'https://discord.com/api/webhooks/XXXXXXX/YYYYYYY?wait=true'
        send_resolved: true

Generate the Discord webhook: Server Settings → Integrations → Webhooks → New Webhook. Copy URL. Raw Prometheus format isn't pretty in Discord — we use the bridge alertmanager-discord or a simple parser that produces clean Discord embeds.

systemd unit /etc/systemd/system/alertmanager.service:

[Unit]
Description=Alertmanager
After=network.target

[Service]
User=alertmanager
Group=alertmanager
Type=simple
ExecStart=/usr/local/bin/alertmanager \
  --config.file=/etc/alertmanager/alertmanager.yml \
  --storage.path=/var/lib/alertmanager \
  --web.listen-address=127.0.0.1:9093

[Install]
WantedBy=multi-user.target
sudo chown alertmanager:alertmanager /etc/alertmanager/alertmanager.yml
sudo systemctl daemon-reload
sudo systemctl enable --now alertmanager

Useful queries for a VPN VPS

In Grafana, these Prometheus queries are the most useful day-to-day:

Total WireGuard bandwidth (Mbps):

rate(wireguard_sent_bytes_total[5m]) * 8 / 1e6 + rate(wireguard_received_bytes_total[5m]) * 8 / 1e6

Top 5 peers by received bytes (24h):

topk(5, increase(wireguard_received_bytes_total[24h]))

Last handshake per peer (humanized):

time() - wireguard_latest_handshake_seconds

CPU utilization %:

100 - (avg by (instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100)

Disk usage %:

100 - (node_filesystem_avail_bytes{mountpoint="/"} / node_filesystem_size_bytes{mountpoint="/"}) * 100

Backup and retention

Prometheus keeps 30 days by default (--storage.tsdb.retention.time=30d). Beyond, either raise retention (add disk) or downsample with Thanos or VictoriaMetrics (overkill for a personal VPS).

/var/lib/prometheus/ burns ~50-100 MB per day for this stack. At 30 days, ~2-3 GB max. Your Contabo VPS has 50 GB NVMe, plenty of margin.

Additional hardening

  • fail2ban on Grafana: build a filter for login attempts. 5× failed auth → 15-min ban.
  • Reverse proxy with basic auth: Caddy or Nginx in front of Grafana if you want to share the dashboard with a colleague without SSH tunnel. But always prefer the WireGuard tunnel if possible.
  • TLS: if you expose Grafana outside LAN/tunnel, certbot + Caddy in 5 minutes.

Verdict

With this stack you get real-time visibility into everything passing on your VPS VPN. You immediately see when a peer pulls 200 GB overnight (probably a Linux ISO, or Plex backup), when CPU spikes (often a misconfigured OOM kill), when a peer hasn't handshaken in 1h (dead or gone client).

It's 30 minutes of setup once, and 14 months of peace with a Contabo VPS S holding its 200 Mbps without surprise.

To get started:

The VPS we use: /go/contabo — 4.99 €/mo, 200 Mbps, German GDPR jurisdiction. Our full Contabo VPS review after 14 months of continuous use.

★ Datacenter Nuremberg GDPR · ✓ IPv4 dédiée incluse · 200+ Mbps garantis

Get Contabo30 jours satisfait ou remboursé