Beszel: lightweight server monitoring with Telegram alerts
Published: 2026-06-11
Grafana and Prometheus are great, but they're heavy. For a single VPS, I use Beszel — a Go binary that gives you CPU, memory, disk, and network charts in a clean UI with almost zero configuration.
What Beszel is
Beszel is a single-binary monitoring dashboard. It has two components:
- Hub — the web UI and data store (runs as a pod in k0s)
- Agent — a lightweight binary that runs on each monitored host and sends metrics to the hub
The hub is deployed via 12-setup-beszel.sh. The agent runs on the host (outside the cluster) as a systemd service.
Hub deployment
yamlcontainers:
- name: beszel
image: henrygd/beszel:latest
ports:
- containerPort: 8090
env:
- name: TZ
value: "Europe/Moscow"
volumeMounts:
- name: data
mountPath: /beszel_data
resources:
requests:
memory: "32Mi"
cpu: "10m"
limits:
memory: "128Mi"
cpu: "200m"
The hub stores its SQLite database in /beszel_data, backed by a hostPath volume at /var/lib/beszel. The resource limits are minimal — 128 MiB is more than enough for a single-host setup.
Telegram alerts
The setup script creates a Kubernetes Secret with the Telegram bot token and chat ID:
bashkubectl create secret generic beszel-telegram \
--from-literal=bot-token="$TG_BOT_TOKEN" \
--from-literal=chat-id="$TG_CHAT_ID"
Beszel reads these and sends a message to the Telegram chat when:
- CPU usage exceeds 80% for more than 5 minutes
- Memory usage exceeds 85%
- Disk usage exceeds 90%
- The agent disconnects (host unreachable)
The last alert is the most useful: if the VPS reboots, crashes, or loses network, Beszel sends a Telegram notification within ~30 seconds.
Agent on the host
The agent binary is installed on the host and runs as a systemd service. It connects to the hub over a WebSocket on port 8090. The hub is exposed internally (no public IngressRoute for the monitoring UI — it's accessed via kubectl port-forward or an SSH tunnel).
bash# Start a local tunnel to the Beszel UI
kubectl port-forward svc/beszel 8090:8090
# Then open http://localhost:8090
What's monitored
- CPU usage per core
- Memory and swap usage
- Disk I/O and usage per filesystem
- Network in/out per interface
- Container stats (via the Docker/containerd socket)
The container stats show per-pod CPU and memory, which overlaps with what Prometheus collects. For quick human inspection, Beszel's charts are faster to read than Grafana dashboards.
Why not just Prometheus + Grafana
On a 4 GB VPS, Prometheus + Grafana + Alertmanager takes about 400-500 MiB of RAM at rest. Beszel hub + agent takes under 50 MiB combined. The tradeoff is that Beszel has no query language, no custom alerting rules, and retention is limited. For a personal server, knowing "CPU is high" is enough — I don't need percentile histograms.
Prometheus is still installed (via the k0s Helm config) because kube-state-metrics data is useful. But Grafana isn't deployed — the raw Prometheus UI is sufficient for the rare times I need to debug cluster internals.
Installation
bash# 12-setup-beszel.sh creates the Deployment and Service
kubectl apply -f k0s-setup/manifests/beszel.yaml
# Install agent on the host
curl -sL https://raw.githubusercontent.com/henrygd/beszel/main/supplemental/install-agent.sh | \
bash -s -- -p 45876 -k "<hub-public-key>"
# Enable and start
systemctl enable --now beszel-agent
The hub generates a public key on first start. Copy it from the Beszel UI under System → Add System → Public key.
Persistent storage
The hub writes metrics to SQLite at /beszel_data. With hostPath, data survives pod restarts but not host reinstalls. For long-term retention, back up /var/lib/beszel periodically:
bash# Cron backup to object storage
0 3 * * * tar -czf /tmp/beszel-backup.tar.gz /var/lib/beszel && \
s3cmd put /tmp/beszel-backup.tar.gz s3://backups/beszel/$(date +%Y%m%d).tar.gz
Exposing the Beszel UI securely
Beszel hub should not be publicly accessible — it shows server metrics and has admin functions. Access via SSH tunnel or kubectl port-forward only:
bash# Option 1: kubectl port-forward
kubectl port-forward -n monitoring svc/beszel 8090:8090
# Option 2: SSH tunnel from local machine
ssh -L 8090:127.0.0.1:8090 user@vps-ip -N
Then open http://localhost:8090 in the browser.
What can go wrong
Agent can't connect to hub.
The hub is not exposed via a public IngressRoute, so the agent uses the node IP and NodePort (or ClusterIP if running on the same host). If the hub service isn't reachable, the agent logs connection refused. Check kubectl get svc -n monitoring beszel and verify the agent's BESZEL_HUB env points to the correct address.
Data is lost after host reinstall.
Beszel uses a hostPath volume at /var/lib/beszel. It survives pod restarts but not host reinstalls. Set up the backup cron from the installation section before you care about historical data.
Telegram alerts stop firing.
Beszel reads the secret once at startup. If the bot token or chat ID changes, restart the pod: kubectl rollout restart deployment/beszel -n monitoring. Verify the alert works with beszel test-alert from inside the pod.
Summary
- Beszel hub + agent uses under 50 MiB combined — a practical alternative to Prometheus + Grafana for a single VPS
- Hub stores metrics in SQLite on a
hostPathvolume; back it up with a cron job - Telegram alerts fire within ~30 seconds for CPU/memory/disk thresholds and agent disconnects
- UI is not exposed publicly — access via
kubectl port-forwardor SSH tunnel - Container stats overlap with Prometheus but are faster to read at a glance