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 · Founder · VPNSmith - Self-host VPN & GDPR VPS specialist8 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 a production-grade setup for a Contabo Cloud VPS 10. node_exporter for the system, wireguard_exporter for peers, Prometheus for storage, Grafana for visualization, Alertmanager + Discord webhook for alerts.

Why monitor a self-host VPN (and not just look at wg show)

Running wg show in a terminal gives you the instant state: connected peers, last handshake, total bytes exchanged since tunnel start. Useful for a one-off debug, but it'll never tell you how much bandwidth you used yesterday at 10pm, which peer monopolizes your upload for three days, or whether your Paris-Nuremberg latency has silently doubled because of a broken BGP peering at the Contabo upstream. For that you need time-series: record a metric every 15 seconds, keep 30 days of history, and be able to query "average throughput between 6pm and 10pm over the past two working weeks". That's exactly what Prometheus does, and that's exactly why Grafana exists on top of it.

The other reason it's underestimated is proactive alerts. Without monitoring, you discover a broken tunnel when a peer sends you a Telegram message saying "it doesn't work for me". With Alertmanager + a Discord webhook, you get the notification within the minute that follows the outage, generally before your users notice. The kinds of incidents this catches early: an OOM on the WireGuard process triggered by a misconfigured peer opening hundreds of parallel connections, a disk silently filling because of broken log rotation, or an upstream cut during an unannounced maintenance. Without monitoring, diagnosing those cases is a struggle.

Finally, this is the foundation of transparency for users. If you host a VPN for your family or company, being able to show them a public read-only dashboard with uptime + consumed bandwidth gives credibility that nobody offers on the consumer side (NordVPN doesn't publish its uptime, ExpressVPN neither).

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

A server room aisle
A server room aisle

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 75 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, for lasting peace of mind on a Contabo Cloud VPS 10 with its advertised 200 Mbit/s link.

To get started:

The VPS we recommend: Contabo Cloud VPS 10 - 5.50 €/mo, advertised 200 Mbit/s link, German GDPR jurisdiction. See our full Contabo VPS review.

★ Nuremberg GDPR datacenter · ✓ Dedicated IPv4 included · 200+ Mbps guaranteed

Self-host your VPN on your own VPS → ContaboFull root access · public IPv4 · pick your region

Frequently asked questions

Does monitoring hit VPS performance?
Marginal. node_exporter + wireguard_exporter + prometheus burn ~80 MB RAM and <1% CPU on a Contabo S VPS. Grafana adds ~150 MB. Everything fits comfortably in 8 GB RAM.
Do I need a dedicated VPS for Prometheus?
Not for personal use. All of it can run on the same VPS as your WireGuard tunnel. For multi-VPS infra (>5 servers), split Prometheus on a dedicated host and scrape over the network.
How to avoid exposing Grafana on the Internet?
Bind Grafana on 127.0.0.1:3000 and access via SSH tunnel (ssh -L 3000:127.0.0.1:3000 vps). Or via the WireGuard tunnel itself (10.66.66.1:3000). No public port open.