
You run a few servers: a game server, a VPS with your website, maybe a small box for a Discord bot. You usually learn that a disk filled up or memory ran out when players or customers tell you, and running htop on each machine only shows the moment you look. You want one dashboard with every server on it, and a message on your phone before something breaks. This guide covers what to watch, three ways to collect it on one monitoring VPS, how to keep the agents' ports closed, and how to know when the monitor itself stops. Commands are for Ubuntu 24.04 and Debian 12.
Quick answer: Run a hub on a small VPS that hosts nothing else and a light agent on every server. For a handful of servers, Beszel is the quickest: one container for the hub, one agent per server, built-in alerts for status, CPU, memory, disk and (since 0.21.1) certificate expiry, and agents that connect out to the hub over HTTPS, so they need no open port. Pick Prometheus with node_exporter, Alertmanager and Grafana for your own alert rules and custom exporters, or Netdata for per-second charts streamed to a Parent node (free plans open 5 nodes in its dashboard). Allow agent ports from the monitor's IP only, and give the monitor a heartbeat to an outside service.
What to Watch on Every Server
Start with five signals. The alerts below are the tools' own defaults and examples, so copy them and tune later.
| Signal | Why it matters | Starting alert |
|---|---|---|
| Up or down | The server, its network or the agent stopped | Unreachable for 5 minutes (Prometheus's InstanceDown example) |
| Disk space | A full disk stops databases, logs and game saves from writing | Under 5% free for 30 minutes, or on course to fill within 24 hours (node_exporter's own rules) |
| Memory | When RAM runs out, the kernel's OOM killer ends a process | Above 90% for 15 minutes (node_exporter's rules) |
| CPU | Long stretches at full load slow everything down | Above 90% for 15 minutes (node_exporter) or 80% for 10 minutes (Beszel's default) |
| Certificates | An expired certificate breaks HTTPS for every visitor | 14 days before expiry (Beszel's and Netdata's default) |
Certificates matter more than they used to. Let's Encrypt stopped sending expiry emails on 4 June 2025, and from 10 February 2027 its certificates last 64 days by default instead of 90, with 45 days to follow in 2028. A renewal that fails quietly leaves you less time to notice.
Three Ways to Do It From One VPS
| Beszel | Prometheus | Netdata | |
|---|---|---|---|
| On each server | Beszel agent | node_exporter | Netdata Agent (child) |
| Connection | Agent dials the hub over HTTPS, or the hub dials the agent on port 45876 | Prometheus scrapes port 9100 on each server | Child streams to the Parent on port 19999 |
| Alerts | Built in, set per system in the web UI | Rules you write, sent by Alertmanager | Stock alerts, run on the Parent |
| Certificates | HTTPS network monitors | blackbox_exporter | x509check collector |
| Catch | Only the metrics on its list | The most setup work | Dashboard opens 5 nodes without a paid plan |
Option 1: Beszel
1. Start the hub behind HTTPS. Point a name such as monitor.example.com at the monitoring VPS and install Docker Engine with the Compose plugin (Ubuntu, Debian). Beszel's security page recommends WebSocket mode with an HTTPS hub URL when hub and agents talk over the internet, because in SSH mode the hub doesn't verify the agent's host key. So the hub runs behind Caddy, in the same layout as our status page guide:
sudo mkdir -p /opt/beszel/conf
cd /opt/beszel
sudo nano compose.yaml
services:
beszel:
image: henrygd/beszel:latest
restart: unless-stopped
environment:
APP_URL: https://monitor.example.com
DISABLE_SSH: "true"
volumes:
- ./beszel_data:/beszel_data
caddy:
image: caddy:2
restart: unless-stopped
ports:
- "80:80"
- "443:443"
- "443:443/udp"
volumes:
- ./conf:/etc/caddy
- caddy_data:/data
- caddy_config:/config
volumes:
caddy_data:
caddy_config:
Create the Caddyfile with sudo nano conf/Caddyfile, using the settings from Beszel's reverse proxy page, then start both with sudo docker compose up -d:
monitor.example.com {
request_body {
max_size 10MB
}
reverse_proxy beszel:8090 {
transport http {
read_timeout 360s
}
}
}
The hub publishes no port, so 8090 is reachable only through Caddy, and DISABLE_SSH stops the hub from dialing agents. Open https://monitor.example.com and create your account straight away.
2. Add each server. Click Add System, enter a name and the server's IP address, and use the binary install command the dialog gives you. Its general form, from the agent docs, takes the public key with -k, the token with -t and the hub URL with -url, and needs root:
curl -sL https://get.beszel.dev -o /tmp/install-agent.sh && chmod +x /tmp/install-agent.sh && sudo /tmp/install-agent.sh -k "<public key>" -t "<token>" -url "https://monitor.example.com"
The script creates a beszel user and a beszel-agent service. By default the agent also runs an SSH server on port 45876 for SSH mode. Switch it off (agent 0.18.4 or later) with sudo systemctl edit beszel-agent, add these lines, then run sudo systemctl restart beszel-agent:
[Service]
Environment="DISABLE_SSH=true"
Click Add System in the dialog; the system turns green once the agent connects. For the Docker agent, put DISABLE_SSH: "true" under environment instead. With many servers, a universal token from /settings/tokens lets agents register without adding each system first.
3. Turn on alerts. Under Settings > Notifications, add a Shoutrrr URL, such as discord://token@webhookid built from a Discord webhook; ntfy, Telegram, Slack and others work too. Then enable alerts per system in the systems table. CPU, memory and disk alerts start at 80 percent for 10 minutes, and you can set the duration from 1 to 60 minutes. Besides status, CPU, memory and disk, version 0.21.1 has alerts for load average, bandwidth, temperature, unhealthy containers, failed systemd services and certificate expiry.
For certificates, open Network Monitors, click Add Monitor, pick a system to probe from and add your site with the HTTP protocol. HTTPS monitors check the certificate since 0.21.0, and the Certificate Expiration alert fires 14 days before expiry by default.
Option 2: Prometheus, node_exporter and Grafana
On each server, install node_exporter (1.7.0 on Ubuntu 24.04, 1.5.0 on Debian 12). The package runs it on port 9100 on every address without a login, so the firewall rule is what keeps it private (UFW setup follows below). Replace 203.0.113.5 with your monitor's IP:
sudo apt update
sudo apt install prometheus-node-exporter
sudo ufw allow proto tcp from 203.0.113.5 to any port 9100
On the monitor, install Prometheus, Alertmanager and blackbox_exporter, which probes your sites and reads their certificates:
sudo apt install prometheus prometheus-alertmanager prometheus-blackbox-exporter
The packaged /etc/prometheus/prometheus.yml already sends alerts to Alertmanager on localhost:9093 and has a node job for localhost:9100. Edit these parts with sudo nano and keep the rest; the blackbox job follows the blackbox_exporter README:
rule_files:
- /etc/prometheus/servers.rules.yml
scrape_configs:
# keep the 'prometheus' job, then:
- job_name: node
static_configs:
- targets: ['198.51.100.10:9100', '198.51.100.20:9100']
- job_name: blackbox
metrics_path: /probe
params:
module: [http_2xx]
static_configs:
- targets: ['https://example.com']
relabel_configs:
- source_labels: [__address__]
target_label: __param_target
- source_labels: [__param_target]
target_label: instance
- target_label: __address__
replacement: 127.0.0.1:9115
Then create /etc/prometheus/servers.rules.yml. The thresholds come from the Prometheus alerting rules page and node_exporter's own alert mixin:
groups:
- name: servers
rules:
- alert: InstanceDown
expr: up == 0
for: 5m
- alert: DiskAlmostFull
expr: (node_filesystem_avail_bytes{fstype!=""} / node_filesystem_size_bytes{fstype!=""} * 100 < 5) and node_filesystem_readonly{fstype!=""} == 0
for: 30m
- alert: DiskFillingUp
expr: (node_filesystem_avail_bytes{fstype!=""} / node_filesystem_size_bytes{fstype!=""} * 100 < 40) and predict_linear(node_filesystem_avail_bytes{fstype!=""}[6h], 24*60*60) < 0 and node_filesystem_readonly{fstype!=""} == 0
for: 1h
- alert: MemoryHigh
expr: 100 - (node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes * 100) > 90
for: 15m
- alert: CpuHigh
expr: sum without(mode) (avg without (cpu) (rate(node_cpu_seconds_total{mode!~"idle|iowait"}[2m]))) * 100 > 90
for: 15m
- alert: SiteDown
expr: probe_success == 0
for: 5m
- alert: CertificateExpiresSoon
expr: probe_ssl_earliest_cert_expiry - time() < 14 * 86400
for: 1h
- alert: Watchdog
expr: vector(1)
Replace /etc/prometheus/alertmanager.yml with a route to Discord. The Discord receiver arrived in Alertmanager 0.25.0, the version in Debian 12 (Ubuntu 24.04 has 0.26.0). The heartbeat route is explained in the monitor section below.
route:
receiver: discord
routes:
- receiver: heartbeat
matchers:
- alertname="Watchdog"
repeat_interval: 5m
receivers:
- name: discord
discord_configs:
- webhook_url: 'https://discord.com/api/webhooks/<id>/<token>'
- name: heartbeat
webhook_configs:
- url: 'https://hc-ping.com/<uuid>'
Check both files, then restart:
promtool check config /etc/prometheus/prometheus.yml
amtool check-config /etc/prometheus/alertmanager.yml
sudo systemctl restart prometheus prometheus-alertmanager
Grafana draws the dashboards. Install it from Grafana's APT repository, as its install page shows, and start it:
sudo apt-get install -y apt-transport-https wget gnupg
sudo mkdir -p /etc/apt/keyrings
sudo wget -O /etc/apt/keyrings/grafana.asc https://apt.grafana.com/gpg-full.key
sudo chmod 644 /etc/apt/keyrings/grafana.asc
echo "deb [signed-by=/etc/apt/keyrings/grafana.asc] https://apt.grafana.com stable main" | sudo tee -a /etc/apt/sources.list.d/grafana.list
sudo apt-get update
sudo apt-get install grafana
sudo systemctl daemon-reload
sudo systemctl enable --now grafana-server
Grafana listens on port 3000 on every address, and its first login is admin with password admin. Reach it through an SSH tunnel (ssh -N -L 3000:127.0.0.1:3000 you@203.0.113.5, then http://localhost:3000) and change the password at once. Add Prometheus as a data source at http://localhost:9090, then under Dashboards, New, Import dashboard, load ID 1860, Node Exporter Full, which expects the job name node.
Option 3: Netdata Parent and Children
Install Netdata on the monitor and on each server. The kickstart script installs nightly builds and turns on daily auto-updates and anonymous statistics by default; --release-channel stable picks stable releases:
wget -O /tmp/netdata-kickstart.sh https://get.netdata.cloud/kickstart.sh && sh /tmp/netdata-kickstart.sh --release-channel stable
On the monitor, which becomes the Parent, generate an API key with uuidgen, open the streaming config, and accept that key from your servers only:
cd /etc/netdata 2>/dev/null || cd /opt/netdata/etc/netdata
sudo ./edit-config stream.conf
[11111111-2222-3333-4444-555555555555]
enabled = yes
allow from = 198.51.100.10 198.51.100.20
On each server, the child, fill in [stream] in stream.conf and switch off the local web server in netdata.conf (sudo ./edit-config netdata.conf), so the child has no open port. Then run sudo systemctl restart netdata on every machine:
# stream.conf
[stream]
enabled = yes
destination = 203.0.113.5:19999
api key = 11111111-2222-3333-4444-555555555555
# netdata.conf
[web]
mode = none
Never set mode = none on the Parent, since it also refuses incoming streams. On the Parent, allow port 19999 from each server (sudo ufw allow proto tcp from 198.51.100.10 to any port 19999) and open its dashboard through an SSH tunnel. The stream is unencrypted unless you add TLS, which Netdata sets up with :SSL after the destination and a certificate on the Parent.
The Parent monitors health and sends alerts for all connected systems. Set notifications there in health_alarm_notify.conf; for Discord that means SEND_DISCORD="YES", DISCORD_WEBHOOK_URL and DEFAULT_RECIPIENT_DISCORD. The stock disk alert warns past 90 percent. For certificates, add a job per site to go.d/x509check.conf, with a name and a source such as https://example.com:443; it warns 14 days before expiry. For servers that stop completely, rely on Netdata Cloud, which notifies about unreachable nodes, or on an outside check. Without a paid plan, the dashboard opens 5 nodes of your choice and shows the rest as Locked; streaming and alert notifications have no such limit.
Close the Agent Ports
| Setup | Open on each server | Open on the monitor |
|---|---|---|
| Beszel | Nothing | 80 and 443 for Caddy |
| Prometheus | 9100, from the monitor's IP only | Nothing |
| Netdata | Nothing | 19999, from your servers only |
Turn UFW on (sudo apt install ufw if it's missing). Its default policy denies incoming traffic, and the ufw manual suggests allowing SSH before you enable it, because enabling can drop existing connections:
sudo ufw allow proto tcp from any to any port 22
sudo ufw enable
sudo ufw status verbose
Ports that Docker publishes skip UFW, because Docker routes them before UFW's chains see the packets (see our Docker and UFW guide); that is why only Caddy publishes ports in the Beszel setup. Everything else on the monitor, such as Prometheus (9090), Alertmanager (9093), blackbox_exporter (9115) and Grafana (3000), stays closed, and you reach the dashboards through an SSH tunnel as in our guide to dashboards without open ports. node_exporter's metrics cross the internet as plain HTTP, so if that matters, scrape over WireGuard or Tailscale (same guide), or give node_exporter TLS and basic auth with --web.config.file, which the project still calls experimental. On a Space-Node VPS you get full root access on KVM, so every rule here is yours to set.
Keep the Monitor Itself Watched
A monitor that dies sends nothing, so give it a heartbeat to a service elsewhere. Healthchecks.io notifies you when an expected ping doesn't arrive, and its free Hobbyist plan monitors 20 jobs (pricing, checked 31 October 2026). Create a check, set its Period to 15 minutes and copy its https://hc-ping.com/<uuid> URL.
- Beszel: add
HEARTBEAT_URL: https://hc-ping.com/<uuid>to the hub'senvironmentand runsudo docker compose up -dagain. Per the heartbeat page, the hub pings every 60 seconds by default, and the POST body lists systems that are down. - Prometheus: the
Watchdogrule always fires, and the Alertmanager route above sends it to the ping URL every 5 to 10 minutes. If Prometheus, Alertmanager or their network breaks, the pings stop. It is the same idea as the Watchdog alert in kube-prometheus. - Netdata, or anything else: add a root cron line with
sudo crontab -ethat pings only while the service runs:
*/5 * * * * systemctl is-active --quiet netdata && curl -fsS -m 10 --retry 5 -o /dev/null https://hc-ping.com/<uuid>
Also run the monitor apart from the machines it watches, ideally at another provider, for the reasons in our status page guide. If your servers run at home or with another host, a small Space-Node VPS in our EU data center can be that separate monitor.
Which One Fits
- A handful of servers and alerts tonight: Beszel. The least setup, alerts in the web UI, certificate checks and its own heartbeat.
- Custom metrics, or Grafana already in use: Prometheus. Any exporter fits, such as the Minecraft plugin in our Grafana and Prometheus dashboard guide, and PromQL lets you alert on anything you collect.
- Per-second detail and auto-discovered services: Netdata, with dashboard access to 5 nodes for free or more on a paid plan.
- Only whether your sites answer from outside: no agents needed. Uptime Kuma does that, as in our Uptime Kuma guide.
FAQ
Can the hub run on one of the servers it watches? It can, but when that server goes down, the dashboard and the alerts go with it. Use a separate small VPS with a heartbeat.
Is it safe to open node_exporter's port 9100?
Not to everyone. The Debian and Ubuntu packages start it with an empty ARGS in /etc/default/prometheus-node-exporter, so it listens on every address with no login. Allow the monitor's IP only, or scrape over a private network.
Why does a server show as down in Beszel while it runs?
Read the hub's log at /_/#/logs and the agent's with sudo journalctl -u beszel-agent -n 50. In WebSocket mode the agent must reach /api/beszel/agent-connect at the hub URL, so check HUB_URL, the token, and that your proxy passes WebSockets, which Caddy does. In SSH mode the hub must reach port 45876, and a hub host with iptables -P FORWARD DROP can break that silently.
Do I still need an outside uptime check? Yes, for anything public. Agents report from inside each server, while an outside check sees DNS, the certificate and the HTTP answer the way visitors do. Run at least one from a machine other than your monitor, such as a status page at another provider.
How long does Prometheus keep data?
15 days by default. For longer, set ARGS="--storage.tsdb.retention.time=90d" in /etc/default/prometheus and run sudo systemctl restart prometheus.
Sources
All opened on 31 October 2026:
- Beszel: What is Beszel, getting started, hub installation, agent installation, environment variables, security, reverse proxy, notifications, Discord URL format, network monitors, heartbeat, common issues, agent install script, alert types in v0.21.1, alert defaults in v0.21.1, release notes v0.21.0 and v0.21.1
- Prometheus: node exporter guide, configuration, alerting rules, functions, prometheus flags, promtool
- node_exporter: README, mixin alerts, mixin thresholds, exporter-toolkit web configuration
- Alertmanager: configuration, changelog, amtool check-config in 0.25.0, repeat check in 0.25.0, kube-prometheus Watchdog runbook and rule
- blackbox_exporter: README, blackbox.yml, prober.go in v0.23.0
- Debian and Ubuntu packages: versions in Debian 12 and Ubuntu 24.04, packaging: prometheus.yml, prometheus service, node exporter defaults, alertmanager.yml, Debian 12 Alertmanager files, prometheus file list
- Grafana: install on Debian or Ubuntu, start the server, sign in, defaults.ini, import dashboards, dashboard 1860
- Netdata: README, kickstart install, Parents, streaming configuration, stream.conf, web server, access and node limits, disk alerts, certificate alerts, x509check collector, Discord notifications, Cloud notifications, start and restart
- Let's Encrypt: Ending expiration notification emails, 64-day certificate lifetimes
- Healthchecks.io: pricing, monitoring cron jobs, configuring checks, pinging API
- Docker and Caddy: Docker and ufw, Compose networking, Caddy image, reverse_proxy
- System tools: ufw(8), systemctl, journalctl, ssh(1), crontab(5), proc_pid_oom_score(5)