Self-Hosted VPS Monitoring Without SaaS Agents: Prometheus, Netdata and Uptime Kuma
Build private VPS monitoring with no third-party agents: node_exporter over WireGuard, Prometheus alerts, Netdata telemetry off, and external uptime checks.
Most monitoring guides end with "install the agent and log into the dashboard." That works, but it means a third party continuously receives your hostnames, IP addresses, process names, disk labels, request rates and traffic patterns. If you run a VPS specifically to keep control of your data, shipping full-fidelity telemetry to a SaaS vendor undoes a large part of that.
This is a practical blueprint for monitoring one to a handful of Linux VPS instances yourself: what to collect, where to run the collector, how to expose metrics without opening ports to the internet, which alerts are actually worth waking up for, and what self-hosted monitoring genuinely cannot tell you.
The short answer
For a small fleet, three layers cover almost everything:
- On-box metrics —
node_exporter(plus service-specific exporters) or Netdata, bound to localhost or a private VPN interface, never to0.0.0.0. - A scraper and alert engine — Prometheus + Alertmanager, ideally on a different machine than the one you care most about.
- External availability checks — Uptime Kuma or a simple cron+curl probe from another network, because a server cannot reliably report its own death.
Everything below is an implementation of those three layers with privacy and small-VPS resource limits in mind.
Layer 1: on-box metrics without exposing them
Bind the exporter to localhost or a VPN interface
The single most common mistake in self-hosted monitoring is running node_exporter on :9100 with no firewall in front of it. That endpoint is unauthenticated and unencrypted by default, and it is genuinely informative to an attacker: kernel version, filesystem layout, mounted devices, network interfaces, uptime, boot time, running systemd units. Internet-wide scanners find these quickly.
Two safe patterns:
Same-host Prometheus — bind to loopback:
# /etc/systemd/system/node_exporter.service.d/override.conf
[Service]
ExecStart=
ExecStart=/usr/local/bin/node_exporter \
--web.listen-address=127.0.0.1:9100 \
--collector.systemd \
--collector.textfile.directory=/var/lib/node_exporter/textfile
Central Prometheus on another box — bind to the VPN address only:
ExecStart=/usr/local/bin/node_exporter --web.listen-address=10.8.0.3:9100
A self-hosted WireGuard tunnel between your servers is the cleanest transport for this. It gives you authenticated, encrypted scraping without TLS certificates, client certs or reverse-proxy auth on every exporter. Add a matching firewall rule so the port is reachable only from the tunnel:
# nftables, inside your inet filter input chain
iifname "wg0" ip saddr 10.8.0.1 tcp dport 9100 accept
If your baseline ruleset is default-deny on input, that one line is all you need. If you are not sure your firewall actually blocks the exporter, verify from an outside host with nc -vz your.ip 9100 rather than trusting the config — and see the nftables baseline ruleset guide if you need a starting point that also survives reboots.
Prometheus vs Netdata
They solve different problems and it is reasonable to run both.
Netdata is per-second resolution, zero-configuration, great for "something is wrong right now, what is it?" It auto-detects nginx, MySQL, Docker and dozens of other services. Downsides: it is heavier than a plain exporter, its short retention is designed for real-time inspection rather than month-over-month trends, and by default it may include telemetry and registry features you probably want off. If you install it, review the relevant options in netdata.conf (anonymous statistics, the registry, and the cloud connection) against the current Netdata documentation and explicitly disable what you do not want. Netdata also honours the DO_NOT_TRACK=1 environment variable during install. Never expose port 19999 publicly — put it behind the VPN or an authenticated reverse proxy.
Prometheus is the right tool for retention, alert rules and comparing "today vs last Tuesday." A minimal scrape config over WireGuard:
global:
scrape_interval: 30s
evaluation_interval: 30s
rule_files:
- /etc/prometheus/rules/*.yml
alerting:
alertmanagers:
- static_configs:
- targets: ["127.0.0.1:9093"]
scrape_configs:
- job_name: nodes
static_configs:
- targets: ["10.8.0.2:9100", "10.8.0.3:9100"]
labels: { env: prod }
A 30-second interval is plenty for infrastructure metrics and cuts storage roughly in half compared to the 15s default. On a small VPS, also cap retention:
--storage.tsdb.retention.time=30d
Layer 2: external checks, because a dead server sends no alerts
Self-hosted monitoring has one structural blind spot: if the VPS goes down, the thing that would have told you also went down. You need at least one probe outside the box.
Uptime Kuma is the pragmatic choice — a single container or Node app that does HTTP, TCP, DNS, ping and TLS-expiry checks and pushes notifications. Run it on a different provider, a different region, or a machine at home. Useful checks per service:
- HTTP(S) check on a real endpoint that touches the database, not a static
/healthfile that returns 200 while the app is broken. - TLS certificate expiry warning at 14 days (catches a silently failing renewal, which is a far more common outage cause than hardware).
- A push/heartbeat monitor for cron-style work: your backup job curls a URL on success, and Kuma alerts when the heartbeat is missing. This detects "the job never ran," which timer-based monitoring inside the box will not.
If you would rather not run another service, a cron job on any second machine gets you 80% of the value:
*/5 * * * * curl -fsS --max-time 10 https://example.com/health >/dev/null \
|| printf 'Subject: health check failed\n\n%s\n' "$(date -Is)" | sendmail [email protected]
Layer 3: alerts worth receiving
An alert you routinely ignore is worse than no alert. Keep the list short and make every rule map to an action you would actually take at 03:00.
groups:
- name: vps-core
rules:
- alert: DiskFillingUp
expr: predict_linear(node_filesystem_avail_bytes{fstype!~"tmpfs|overlay"}[6h], 24*3600) < 0
for: 30m
annotations:
summary: "{{ $labels.instance }} {{ $labels.mountpoint }} predicted full within 24h"
- alert: DiskCritical
expr: node_filesystem_avail_bytes{fstype!~"tmpfs|overlay"} / node_filesystem_size_bytes < 0.08
for: 10m
- alert: MemoryPressure
expr: node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes < 0.10
for: 15m
- alert: HighCPUSteal
expr: avg by (instance) (rate(node_cpu_seconds_total{mode="steal"}[15m])) > 0.10
for: 1h
- alert: SystemdUnitFailed
expr: node_systemd_unit_state{state="failed"} == 1
for: 5m
- alert: UnexpectedReboot
expr: time() - node_boot_time_seconds < 600
for: 0m
- alert: BackupStale
expr: time() - node_backup_last_success_timestamp_seconds > 30*3600
for: 0m
Two of these deserve comment.
predict_linear on free disk space is the highest-value alert on a typical VPS. Runaway logs, an unrotated journal or a growing database will fill a 40 GB volume quietly, and a full root filesystem breaks everything at once. Pair it with real retention limits — the VPS log minimization guide covers journald caps and log rotation that keep this from becoming a recurring incident.
Sustained CPU steal above roughly 10% means you are waiting on other tenants, not on your own workload. Treat it as a provider/node-quality signal rather than an application bug, and confirm with the deeper measurements in how to tell if your VPS is oversold before you open a ticket.
Exporting your own metrics with the textfile collector
node_backup_last_success_timestamp_seconds above is not built in — you write it. The textfile collector turns any script into a metric source, which is the simplest way to monitor cron jobs, certificate expiry, queue depth or anything else you can measure in shell:
#!/bin/bash
# runs after your restic/borg job succeeds
out=/var/lib/node_exporter/textfile/backup.prom
printf 'node_backup_last_success_timestamp_seconds %s\n' "$(date +%s)" > "$out.$$"
mv "$out.$$" "$out" # atomic; avoids scraping a half-written file
Alerting on backup freshness rather than backup exit codes is the difference between believing you have backups and knowing you do. It still does not prove the archive is restorable, which is why periodic restore drills — as described in the guide to encrypted off-site backups with restic or Borg — remain non-negotiable.
Keeping the monitoring stack small
On a 1–2 GB VPS, monitoring can easily become the largest consumer of memory. Realistic mitigations:
- Run Prometheus and Grafana on the least critical node, or on a cheap dedicated monitoring VPS. Do not put them on the production database host.
- Skip Grafana entirely if you rarely look at dashboards. Prometheus' own expression browser plus Alertmanager covers a small fleet.
- Trim collectors:
--no-collector.arp --no-collector.infiniband --no-collector.zfsand similar reduce cardinality and scrape cost. - Avoid high-cardinality labels. A label containing request paths, user IDs or container IDs will inflate your time-series count far faster than adding servers.
- Set explicit memory limits with systemd so a monitoring runaway cannot take down the box.
What self-hosted monitoring does not give you
Be honest about the limits:
- It does not hide your infrastructure from your provider. Your host can see traffic volumes and, on unencrypted disks, the data itself. Monitoring privacy is about not adding a third observer.
- It is not a security monitoring system. Metrics show symptoms — CPU spikes, unexpected outbound traffic, new listening ports — not intrusion. Anomalous graphs are a prompt to investigate, and the response process is a separate discipline.
- A single external prober is not a network view. One check from one location cannot distinguish "your site is down" from "the route between that prober and your VPS is broken." Two probers on different networks resolve most ambiguity.
- Metrics endpoints are sensitive. Leaving
/metricsor a Netdata dashboard public gives away your entire software inventory and, over time, your traffic patterns. Treat it like SSH: private interface, firewall, no exceptions.
Common mistakes
- Binding exporters to all interfaces and relying on "nobody knows the port."
- Alerting on CPU utilization. High CPU is often correct behaviour; latency, saturation and queue depth are the signals that matter.
- Monitoring only from inside the server, then discovering downtime from a user email.
- Setting
for: 0mon noisy conditions, producing alert fatigue within a week. - Forgetting to monitor the monitor — give Prometheus/Uptime Kuma its own heartbeat check.
- Running Prometheus with unbounded retention on a 20 GB disk, then triggering the very disk-full outage you built it to prevent.
FAQ
Is Prometheus overkill for one VPS? For a single server, Netdata plus an external Uptime Kuma probe is often enough. Add Prometheus when you want alerts based on trends, retention beyond a few hours, or a second server to compare against.
Can I scrape metrics over the public internet safely? Yes, with TLS and authentication on the exporter or a reverse proxy in front of it, but a VPN is simpler to get right and leaves nothing unauthenticated if a config change slips through.
Does monitoring need a lot of disk? Prometheus stores roughly one to two bytes per sample after compression. A handful of nodes at a 30s interval with 30-day retention typically stays in the low single-digit gigabytes — cardinality, not node count, is what blows this up.
Which alerts should I start with? Disk space prediction, disk critical, external HTTP failure, TLS expiry, failed systemd units and backup staleness. That set catches the majority of real-world VPS incidents.
Takeaway
Private monitoring is not harder than the SaaS route, it just moves the work from account signup to firewall rules. Bind exporters to loopback or a WireGuard interface, scrape from a machine that is not the one you are most worried about, keep the alert list short enough that every page means something, and always run one probe from outside your own infrastructure.
If you are building this on an IronBalkans VPS, the full-root KVM environment gives you what this stack needs — your own kernel, working WireGuard, and no agent you did not install. Running your monitoring node on a separate instance, with anonymous crypto-paid signup and no KYC, also keeps the observer inside your own perimeter rather than in a vendor's analytics pipeline.
