NeuroDynamic.Tech
Sign inJoin

Know when things break: a tiny monitoring setup

Two small dashboards watching your server: Beszel graphs the machine itself, Uptime Kuma watches your services. In my test the stack used about 155 MB of RAM at idle.

The founder · 11 min read ·

BASIC/GOING FURTHER

Tested for real: I ran every command in this guide top-to-bottom on a fresh Ubuntu 24.04 virtual machine (4 CPUs, 8 GB RAM) on 13 August 2026. Versions at test time: Docker Engine 29.7.2, Beszel 0.18.7 (hub and agent), Uptime Kuma 2.5.0. The compose file below uses latest tags, so your versions may be newer — pin versions if you need repeatable installs.

Found a problem? Tell me via the contact page.

What you'll end up with

Two small dashboards watching your server. Beszel graphs the machine itself — CPU, memory, disk, per-container usage. Uptime Kuma watches your services. With notifications configured, it can tell you when one stops answering. In my test, I killed a service on purpose and Uptime Kuma flagged it DOWN within 20 seconds. In my test, the stack used about 155 MB of RAM at idle.

2026-08-13 16:17:23  monitor=1  status=UP    msg='200 - OK'
   [service killed here]
2026-08-13 16:17:43  monitor=1  status=DOWN  msg='connect EHOSTUNREACH ...'

Who this is for

BASIC steps, one GOING FURTHER section. The install is copy-paste. Linking Beszel's agent (step 3) has one copy-a-key-between-screens moment — that section is marked. Prerequisite: the Docker install guide.

Time and cost

About 20 minutes of machine time in my test. Allow 50 minutes end to end, including clicking through both apps' setup screens. Free to run yourself, apart from your server costs. Roughly 700 MB of disk for the three images.

A note on how I tested this

My test rig has no browser. Where you will click buttons in a web page, I drove the same actions through each app's own API and captured the real responses. Every backend state change I rely on here — accounts created, agent linked, monitor added, the DOWN detection — happened for real and is in the transcripts. The screen descriptions below are based on each app's UI and docs, not screenshots from my test rig. The outputs are what the machine actually said.

Words you'll meet

  • Monitor — one thing Uptime Kuma checks repeatedly: "is this URL answering?". See /basics.
  • Heartbeat — one check result. A row of green heartbeats is a healthy service.
  • Hub and agent — Beszel's two halves. The agent reads the machine's vitals; the hub collects and graphs them.
  • Named volume — Docker-managed storage that outlives containers. Both apps keep their history in one.
  • docker.sock — Docker's control socket. We hand the agent a read-only view of it so it can report per-container stats.
  • Public key — the shareable half of a key pair. The hub shows you one; the agent uses it to know which hub to trust.

Placeholders

YOUR-SERVER-IP — your server's LAN address. YOUR-PASSWORD-HERE — a password you choose. Everything else is copy-paste.

Before you start

  • Docker installed per the Docker install guide.
  • A browser on your laptop that can reach the server.

Firewall note: this compose file publishes ports 8090 and 3001 to your whole network, so your laptop can reach the dashboards. If you run UFW, know that it does not block Docker's published ports — that surprise gets a full guide: the firewall trap.

Step 1 — Write the compose file

One folder, one file, three services.

mkdir -p ~/monitoring
nano ~/monitoring/compose.yaml

Paste this and save:

services:
  beszel:
    image: henrygd/beszel:latest
    container_name: beszel
    ports:
      - "8090:8090"
    volumes:
      - beszel-data:/beszel_data
    restart: unless-stopped

  beszel-agent:
    image: henrygd/beszel-agent:latest
    container_name: beszel-agent
    environment:
      LISTEN: "45876"
      KEY: "PASTE-KEY-HERE"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
    restart: unless-stopped

  uptime-kuma:
    image: louislam/uptime-kuma:2
    container_name: uptime-kuma
    ports:
      - "3001:3001"
    volumes:
      - kuma-data:/app/data
    restart: unless-stopped

volumes:
  beszel-data:
  kuma-data:

PASTE-KEY-HERE stays as it is for now. The hub hands us the real key in step 3.

Step 2 — Start the hub on its own

The agent can't start properly without its key, so bring up just the hub first.

cd ~/monitoring
docker compose up -d beszel

A passing run includes:

 Container beszel  Created
 Container beszel  Started

Check it worked.

curl -s -o /dev/null -w "%{http_code}\n" http://localhost:8090
200

Browser steps in this section are described from the app's UI; I verified the resulting state changes through Beszel's API.

Open http://YOUR-SERVER-IP:8090 in your browser. Beszel greets you with a create-account form — pick an email and password. (Locked out later? docker exec beszel /beszel superuser upsert [email protected] YOUR-PASSWORD-HERE resets it; the real response is Successfully saved superuser.)

Now click Add System and fill in:

  • Name: my-server (or anything)
  • Host: beszel-agent — the service's name in the compose file; Docker's internal network resolves it
  • Port: 45876

The dialogue also shows a public key — one long line starting ssh-ed25519 AAAA.... Copy it. Don't click save yet.

Edit the compose file and replace PASTE-KEY-HERE with that line (keep the quotes):

nano ~/monitoring/compose.yaml

Start the rest of the stack — the agent now boots with its key, and Uptime Kuma comes along:

docker compose up -d
 Container beszel-agent  Started
 Container uptime-kuma  Started

Back in the browser, save the Add System dialogue.

Check it worked. The new system shows up within about 20 seconds, and the graphs start moving. In my test, the record flipped from "status": "pending" to "status": "up" and the first stats arrived 11 seconds later — real CPU, memory and disk figures for the machine:

"stats": { "cpu": 4.61, "m": 7.76, "mu": 0.73, "d": 60.95, "du": 4.72, ... }

That's 4.61% CPU, 0.73 of 7.76 GB memory used, 4.7 of 61 GB disk. Click the system to see it as graphs, including one line per container.

Step 4 — Set up Uptime Kuma

Same testing note as step 3: browser labels from the UI, state changes verified via the API.

Open http://YOUR-SERVER-IP:3001. Uptime Kuma (version 2) first asks which database to use — choose SQLite, the embedded option, right for a single small server. It sets itself up (about ten seconds), then shows a create-account form. Make your admin account.

Check it worked. You land on an empty dashboard, and the address bar now says /dashboard. From the server:

curl -s -o /dev/null -w "%{http_code} %{redirect_url}\n" http://localhost:3001
302 http://localhost:3001/dashboard

Step 5 — Add a monitor

Click Add New Monitor and set:

  • Monitor type: HTTP(s)
  • Friendly name: Beszel hub
  • URL: http://beszel:8090 — again the compose service name; the two containers share a network
  • Heartbeat interval: 20 seconds

Save. Yes — we're pointing the watcher at the other watcher. It's a real service on your machine, and it makes the next step wonderfully easy to see.

Check it worked. Green heartbeats appear, one every 20 seconds. The machine's view of the same thing, from my test:

2026-08-13 16:16:43  monitor=1  status=UP  msg='200 - OK'
2026-08-13 16:17:03  monitor=1  status=UP  msg='200 - OK'

Step 6 — Break something on purpose

A monitor you have never seen go red is a decoration. Kill the Beszel hub:

docker stop beszel
beszel

Watch the Uptime Kuma dashboard. Within one heartbeat interval, the monitor goes red. My captured stream:

2026-08-13 16:17:23  monitor=1  status=UP    msg='200 - OK'
2026-08-13 16:17:43  monitor=1  status=DOWN  msg='connect EHOSTUNREACH 172.18.0.3:8090'

Twenty seconds from kill to detection. Now bring it back:

docker start beszel

Check it worked. The monitor returns to green:

2026-08-13 16:19:02  monitor=1  status=UP  msg='200 - OK'

Step 7 — Count the cost

Monitoring that eats the server it watches is self-defeating. Ask Docker what the stack uses:

docker stats --no-stream

You should see something like:

NAME           CPU %     MEM USAGE / LIMIT     MEM %
uptime-kuma    0.97%     141.2MiB / 7.755GiB   1.78%
beszel         0.00%     9.824MiB / 7.755GiB   0.12%
beszel-agent   0.00%     3.926MiB / 7.755GiB   0.05%

155 MB all in, and near-zero CPU at idle. Uptime Kuma is the heavy one; Beszel's pair is remarkably light.

What about notifications?

Detection without notification only helps if you stare at dashboards, so this matters — and I'll be straight: I did not wire up a notification channel in this test, so unlike everything above, this part is described, not proven. In Uptime Kuma: Settings → Notifications → Setup Notification opens a long list of services — Telegram, email, Signal, ntfy, and more. Each form has a Test button; do not trust a channel until its test message lands on your phone. Beszel documents its own alerts (per-system thresholds for CPU, memory, disk, status) under each system's bell icon — also untested here.

Something went wrong?

  • You see the Beszel system stuck on "pending" → it means the hub can't reach the agent, or the agent rejected the key → run docker logs beszel-agent; if the KEY line still says PASTE-KEY-HERE, fix it and docker compose up -d again.

  • Uptime Kuma asks which database to use → it means you're on version 2, which is correct → choose SQLite and carry on; no external database is needed.

  • You see the monitor DOWN with EHOSTUNREACH while everything looks fine → it means the target container is genuinely stopped or unreachable on the shared network → docker ps and check the target's name matches the URL you gave the monitor.

  • A dashboard page will not open at :8090 or :3001 → it means the container is not running or the port is not reachable → docker ps, then docker logs beszel (or uptime-kuma), then check you used the server's IP, not localhost, from your laptop.

  • docker compose up -d fails with permission denied on docker.sock → it means your user is not in the docker group yet → log out and back in, or revisit the Docker install guide.

  • port is already allocated → it means another service is using 8090 or 3001 → change the left-hand side of the port mapping in the compose file and re-run.

Undo all of this

Tested for real. From the folder, remove containers, network and both data volumes (dashboards, history, accounts — all of it):

cd ~/monitoring
docker compose down -v
 Container beszel-agent  Removed
 Container beszel  Removed
 Container uptime-kuma  Removed
 Volume monitoring_beszel-data  Removed
 Volume monitoring_kuma-data  Removed

Remove the images and the folder:

docker image rm henrygd/beszel:latest henrygd/beszel-agent:latest louislam/uptime-kuma:2
rm -r ~/monitoring

Check it worked. docker ps -a lists no monitoring containers and docker volume ls no monitoring volumes. Both came back empty in my test.

Where to go next


Last tested: 2026-08-13 on Ubuntu 24.04.4. Versions: Docker Engine 29.7.2, Beszel 0.18.7, Uptime Kuma 2.5.0.


Tried it? Improved it?

Tell the forum what worked and what didn’t: real experience beats recommendations, and the best answers get folded back into this guide with credit.

Related guides