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
In Grafana:
- Settings → Data Sources → Add data source → Prometheus
- URL:
http://127.0.0.1:9090 - Save & Test → "Data source is working"
Step 7 - Import dashboards
Ready-made dashboards:
- Node Exporter Full: ID
1860on grafana.com - full system metrics - WireGuard: ID
12557or 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:
- Contabo VPS + WireGuard setup in 20 min
- Ready-to-use WireGuard templates
- Netfilter kill switch to never leak
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→

