How to Deploy Beszel on a VPS
Updated Aug 2026
verified on Ubuntu 26.04 · Aug 2026Self-host Beszel — a lightweight server-metrics hub with one agent per machine — on a small VPS, with HTTPS, alerts and an honest note on what it does not monitor.
- A small VPS for the hub — 1 vCPU / 512 MB–1 GB RAM is plenty
- A fresh Ubuntu 24.04 or 26.04 server with root/sudo SSH access
- Root or sudo on every machine you want to monitor (the agent installs there)
- A domain you can point at the hub
- Docker Engine + Compose installed (see the base guide below)
What Beszel is — and what it isn't
Beszel is a lightweight server-monitoring hub. You run one small hub somewhere, install a small agent on every machine you care about, and the hub charts CPU, memory, disk, network, load average, temperature and per-container Docker/Podman stats, keeps history, and alerts you when a threshold is crossed or a system goes quiet.
The hub is a single Go binary built on PocketBase, so there is no separate database to provision — everything lives in one data directory. That, plus a genuinely pleasant dashboard, is the appeal: it gives you most of what people install a full Prometheus and Grafana stack for, in about ten minutes and a couple of hundred megabytes of RAM.
Now the part that saves you an afternoon: Beszel does not check URLs. It has no HTTP endpoint monitor, no ping-a-website check, no "is my site up" test of any kind. It never makes an outbound request to something it doesn't have an agent on. It watches machines, not services. If your web server is running fine but your app is returning 500s, Beszel will show you a healthy green box.
That is not a shortcoming — it's the scope of the tool — but it means Beszel is usually half of a monitoring setup. Pair it with an uptime monitor that pokes your endpoints from outside: Uptime Kuma if you want to click through a UI, Gatus if you want the checks in version control. Uptime Kuma vs Beszel walks through which questions each one answers. The two together cost you one extra container and cover both halves: is the box healthy and is the service actually answering.
The shape of the deploy
Two pieces, installed separately, and it's worth having the picture before you start:
- The hub — one container, a web UI, all history and users. You install this once.
- The agent — one per monitored machine, including the hub's own box if you want to see it. It exposes metrics for the hub to read.
The connection between them is the step people get stuck on, because it is deliberately manual: you add a system in the hub UI first, the hub generates a public key and a token, and you paste those two values into the agent installer on the target machine. There is no auto-discovery, and there is no way to skip ahead — the key doesn't exist until you've created the system in the UI. Budget a couple of minutes per machine.
Server sizing
The hub is tiny. An embedded database, some charts, and a handful of connections:
- 512 MB RAM / 1 vCPU — fine for a personal fleet of a few machines.
- 1 GB RAM — the comfortable default, and what to pick if the hub box also runs a reverse proxy and other small services.
- 2 GB RAM+ — only for a large fleet or a long retention window.
Disk: 10–20 GB is plenty. The database grows with systems × retention, and the records are small.
The agent is smaller still — a single binary reading /proc and the Docker
socket. It adds a negligible footprint to whatever it's installed on.
Where to put the hub: on a box separate from your main workload, same as any monitor. If the hub lives on the server it watches, it goes down exactly when you need it. A Hetzner CX22, a small Kamatera instance, or a base DigitalOcean droplet are all more machine than the hub needs.
Prepare the server
This guide assumes Docker Engine and the Compose plugin are installed, along
with a non-root deploy user and a ufw firewall. If not, work through
Docker & Compose on Ubuntu first.
On the hub box, open SSH and the reverse proxy ports only:
sudo ufw allow OpenSSH
sudo ufw allow 80
sudo ufw allow 443
sudo ufw enable
sudo ufw status verbose
The hub's own port stays on loopback behind the proxy. Agent firewall rules come later, once you know which direction your agents will connect — that section is below and it matters more than it sounds.
Install the hub
One container, one data directory. That directory is the entire application: users, systems, history, settings, all of it.
A single command is enough to try the hub. Stop and remove that container before the compose step below, or the two will contend for the same name and port:
docker run -d --name beszel --restart unless-stopped \
-p 8090:8090 -v /opt/beszel:/beszel_data henrygd/beszel:latest
That's the whole install, and it's enough to get to the UI at
http://SERVER_IP:8090. For anything you intend to keep, use Compose instead so
the hub is declarative and bound to loopback:
mkdir -p ~/beszel && cd ~/beszel
cat > docker-compose.yml <<'YAML'
# docker-compose.yml
services:
beszel:
image: henrygd/beszel:latest
container_name: beszel
restart: unless-stopped
volumes:
- /opt/beszel:/beszel_data
ports:
# Loopback only — the reverse proxy is the sole public door.
- "127.0.0.1:8090:8090"
YAML
docker compose up -d
docker compose logs --tail 50
Note the data directory is a host bind mount (/opt/beszel), not a named
volume. That's deliberate: it's the single thing you back up, and having it at a
known path on the host makes both the backup and the restore obvious.
HTTPS + domain
Point an A record for beszel.example.com at the hub's public IP, wait for
it to resolve, then terminate TLS in front of 127.0.0.1:8090. The simplest
path is Automatic HTTPS with Caddy:
beszel.example.com {
reverse_proxy 127.0.0.1:8090
}
Do this before you create the admin account. The hub is a normal HTTP service with no special proxy requirements, but it is also where every credential and every hostname in your fleet ends up — creating that first account over plain HTTP means posting the password in clear text across the internet.
If Caddy runs as a container, 127.0.0.1 is the proxy's own loopback. Put both
services in one compose file and use reverse_proxy beszel:8090, dropping the
host port publish.
First login
Load https://beszel.example.com. The first screen creates the admin
account — there is no default password, and once that account exists the
create-first-user path is closed. Use a strong, unique password: this dashboard
is a map of your infrastructure.
Then, before you add machines, go into the settings and check the user options. The hub is built on PocketBase and inherits its user model, so it's worth confirming that account creation is closed to the public and that the only users are ones you made. If you want other people to have logins, create them explicitly rather than opening registration.
Add your first system
This is the manual handshake described earlier, and the order matters.
In the hub UI, click Add System. You'll be asked for a name and a host/IP, and the dialog hands you back two things: a public key and a token. Keep that dialog open — you need both values on the target machine before you can finish.
On the machine you want to monitor, run the agent installer:
curl -sL https://get.beszel.dev -o /tmp/install-agent.sh \
&& chmod +x /tmp/install-agent.sh \
&& /tmp/install-agent.sh
The script asks for the public key (and, depending on how you're connecting, the
token and the hub URL), installs the beszel-agent binary, and registers a
beszel-agent systemd service set to restart on failure. Confirm it came up:
systemctl status beszel-agent
journalctl -u beszel-agent --no-pager -n 50
Back in the hub, finish adding the system. Within a minute or so the first metrics should appear.
Docker container stats are the one thing that needs a permission: the agent must
be able to read the Docker socket. The installer handles the common case, but if
container charts stay empty on a machine, that's the thing to check — the agent
needs read access to /var/run/docker.sock.
Repeat for every machine, including the hub's own box if you want to see it in the list. Each one is a fresh Add System in the UI (each gets its own token) plus one installer run.
Which way does the connection go?
Beszel supports two connection directions, and knowing which one you're using decides your firewall rules:
- Agent connects out to the hub (a WebSocket connection, configured with a
HUB_URLand the token). This is the friendlier option: the agent needs outbound access to the hub's HTTPS port and nothing needs to be opened inbound on the monitored machine. This is what you want for machines behind NAT, on home connections, or at another provider. - Hub connects in to the agent, over SSH-style transport to the agent's listening port — 45876 by default. Here the hub needs network access to the agent, so the monitored machine must accept inbound connections on that port from the hub's IP.
If you use the second mode, do not open 45876 to the world. Restrict it to the hub:
sudo ufw allow from HUB_IP to any port 45876 proto tcp
Better still, run the traffic over a private network or a mesh VPN — see Deploy NetBird on a VPS or Deploy Headscale on a VPS — and keep the agent port off the public internet entirely.
Alerts
Metrics without alerts are a wall of pretty charts you look at after the incident. Alerts are configured per system and per alert type, and Beszel covers the ones that matter for machines:
- Status — the system stopped reporting. This is the one to enable on everything, first, because it's the "the box is gone" signal.
- CPU, memory, disk, bandwidth, and load average thresholds.
- Temperature and fan speed, where the agent can read them.
- Disk health via S.M.A.R.T. on hardware that exposes it.
The two that earn their keep on a VPS fleet are status and disk usage — a full disk is the most common self-inflicted outage in self-hosting, and it's the one a threshold alert genuinely prevents.
Set thresholds with a little headroom and a sustained duration where offered. An alert that fires on every backup job's CPU spike gets muted, and a muted alert is worse than no alert.
Backups
Everything the hub knows lives in the data directory you bind-mounted:
docker compose stop
tar czf beszel-$(date +%F).tar.gz -C /opt/beszel .
docker compose start
Stopping the hub first gives a consistent copy of the embedded database. The hub is light enough that a few seconds of downtime costs nothing.
Beszel also has built-in backups in its settings, which can write to disk or to S3-compatible object storage on a schedule, and restore from the same place. That's the better long-term answer — configure it once and you get scheduled, off-box copies without a cron job of your own. Use the tar command above for a quick snapshot before an upgrade.
The agents need no backup. They hold no state worth keeping; reinstalling one is a fresh Add System and one installer run.
Copy whichever archive you use off the box. A backup that only exists on the server it backs up dies with the server.
Upgrades
The hub, if you're on Compose:
cd ~/beszel
docker compose pull
docker compose up -d
The agent, on each monitored machine, is a binary managed by systemd rather than a container. Re-running the installer updates it, and the installer can also configure the agent to update itself automatically — worth turning on for a fleet, since a stale agent is easy to forget about.
Keep hub and agents roughly in step. Back up before a hub upgrade — the embedded database migrates on startup and migrations are one-way.
Troubleshooting
A system sits at "pending" or never connects. This is the connection
direction problem nine times out of ten. If the hub connects to the agent, the
agent's port (45876 by default) must be reachable from the hub — check ufw on
the monitored machine and any cloud firewall in front of it. If the agent
connects out to the hub, check that it can reach the hub's URL:
curl -I https://beszel.example.com from the monitored box.
The agent service is running but the hub shows nothing. Almost always a
key/token mismatch — the values are per system, so pasting the key from a
different system's dialog fails silently in exactly this way. Check
journalctl -u beszel-agent -n 50, then delete the system in the hub and add it
again for a fresh pair.
Docker container charts are empty, host metrics are fine. The agent can't
read the Docker socket. Give it read access to /var/run/docker.sock and
restart the service.
Temperature or S.M.A.R.T. data is missing. Perfectly normal on a VPS: the hypervisor doesn't expose physical sensors or raw disk health to the guest. This is not a bug you can configure away — those panels are for bare metal.
Everything is green but the website is down. Working as designed, and worth repeating: Beszel doesn't check URLs. That question needs an uptime monitor alongside it.
Verification + next steps
You're done when you can: load https://beszel.example.com over a valid
certificate, log in, see every machine reporting live CPU and memory, enable a
status alert, then stop an agent on purpose and receive the notification. Until
you've watched an alert arrive, you have charts, not monitoring.
From there: install the agent on every box you own — it's cheap enough that there's no reason to leave any machine unwatched — turn on the built-in scheduled backup to object storage, and close the gap this tool deliberately leaves by running an endpoint monitor next to it (Uptime Kuma or Gatus). For the ranked host picks, see Best VPS for Monitoring & Uptime.