Cara Membuat Dashboard Monitoring Server (NOC) dengan Uptime Kuma dan Beszel

Dashboard monitoring server NOC dengan Uptime Kuma dan Beszel

Pernah lihat ruang kontrol bandara? Di dinding ada layar besar yang menampilkan semua pesawat sekaligus. Dengan begitu, petugas tidak perlu membuka jadwal penerbangan satu per satu. Cukup melirik layar, lalu langsung tahu mana yang terlambat. Ruangan seperti itu disebut NOC (Network Operations Center). Selain itu, prinsip yang sama bisa Anda terapkan untuk server: satu dashboard monitoring server yang menampilkan status semua website dan server di satu layar.

Masalah yang sering terjadi di tim kecil: website mati baru ketahuan setelah pelanggan komplain, atau disk server penuh diam-diam sampai database berhenti menulis. Padahal tool monitoring gratis sudah banyak. Namun, datanya terpencar di beberapa aplikasi dan tidak ada yang membukanya setiap saat.

Pada tutorial ini kita membangun dashboard NOC seperti yang kami pakai sehari-hari di kantor: Uptime Kuma mengecek website dari luar, Beszel membaca CPU, RAM, dan disk dari dalam server, lalu sebuah collector Python kecil menggabungkan keduanya menjadi satu halaman yang tampil terus di layar TV. Selain itu, semua yang ditulis di sini berasal dari setup yang benar-benar berjalan di server produksi kami. Screenshot asli sudah disensor, sedangkan IP address dan domain di contoh kode diganti dengan nilai contoh.

Hasil akhir dashboard monitoring server NOC berisi status website dan server
Screenshot asli dashboard NOC kami: 22 website/layanan dan 10 server dalam satu layar. IP address dan domain disensor.

Kenapa Perlu Dashboard NOC Sendiri?

Uptime Kuma dan Beszel masing-masing sudah punya tampilan web yang bagus. Namun, untuk layar dinding keduanya punya kekurangan: harus login, sesi bisa kedaluwarsa, dan Anda harus bolak-balik dua tab. Oleh karena itu, dashboard NOC menyelesaikannya dengan satu halaman ringan yang:

  • Menampilkan status website (dari Uptime Kuma) dan server (dari Beszel) berdampingan.
  • Memunculkan banner merah besar saat ada gangguan, jadi terlihat dari jauh.
  • Tidak butuh login, tapi tetap terkunci dengan key rahasia.
  • Sangat ringan, cocok dijalankan berbulan-bulan di PC mini atau TV box.

Komponen yang Dipakai

KomponenFungsiCara mengambil data
Uptime KumaCek website, API, port TCP, ping, dan masa berlaku SSL dari luarCollector membaca SQLite kuma.db (read-only)
Beszel HubMenerima metrik CPU, RAM, disk, dan network dari agentCollector memanggil REST API PocketBase
beszel-agentProgram kecil di tiap server yang mengirim metrik ke hubKoneksi WebSocket keluar ke hub
collector.pyMenggabungkan dua sumber menjadi data.json setiap 30 detikPython 3 bawaan, tanpa library tambahan
Halaman NOCHTML + JavaScript yang membaca data.jsonDisajikan Nginx di /noc/
Arsitektur dashboard monitoring server dengan Uptime Kuma, Beszel, dan collector Python
Alur data: Uptime Kuma dan Beszel bekerja sendiri-sendiri, collector hanya membaca hasilnya.

Selanjutnya, ada satu prinsip penting: taruh monitoring di server yang berbeda dari yang dipantau. Kalau monitoring ikut mati bersama server utama, akibatnya tidak ada yang memberi tahu Anda. Selain itu, VPS kecil 2 core dan 2 GB RAM sudah lebih dari cukup.

Persiapan

KebutuhanContoh di tutorial
VPS Ubuntu 24.04 (terpisah dari server yang dipantau)IP 203.0.113.20
Subdomain untuk panelmonitor.contoh.com (Uptime Kuma + NOC), beszel.contoh.com
Nginx + CertbotReverse proxy dan SSL Let’s Encrypt
Node.js 20 ke atasUntuk Uptime Kuma 2.x
Bot Telegram (opsional)Untuk notifikasi saat ada gangguan
sudo apt update && sudo apt install -y nginx certbot python3-certbot-nginx git jq sqlite3
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt install -y nodejs

Langkah 1: Pasang Uptime Kuma

Pertama, kita jalankan Uptime Kuma tanpa Docker, dengan user sistem khusus supaya tidak berjalan sebagai root:

sudo useradd -r -M -d /opt/uptime-kuma -s /usr/sbin/nologin uptimekuma
sudo git clone https://github.com/louislam/uptime-kuma.git /opt/uptime-kuma
sudo chown -R uptimekuma:uptimekuma /opt/uptime-kuma
cd /opt/uptime-kuma
sudo -u uptimekuma npm run setup     # checkout rilis stabil terbaru + download file frontend

Service systemd Uptime Kuma

Database Uptime Kuma nanti otomatis dibuat di /opt/uptime-kuma/data/kuma.db. Path ini nantinya dibaca collector di Langkah 4. Setelah itu, buat service systemd /etc/systemd/system/uptime-kuma.service:

[Unit]
Description=Uptime Kuma
After=network-online.target
Wants=network-online.target

[Service]
User=uptimekuma
Group=uptimekuma
WorkingDirectory=/opt/uptime-kuma
Environment=NODE_ENV=production
Environment=UPTIME_KUMA_HOST=127.0.0.1
Environment=UPTIME_KUMA_PORT=3001
ExecStart=/usr/bin/node server/server.js
Restart=always
RestartSec=10
MemoryMax=400M
AmbientCapabilities=CAP_NET_RAW
NoNewPrivileges=true

[Install]
WantedBy=multi-user.target

UPTIME_KUMA_HOST=127.0.0.1 membuat Uptime Kuma hanya bisa diakses dari server itu sendiri. Dengan demikian, akses dari luar selalu lewat Nginx yang sudah memakai HTTPS. Selain itu, CAP_NET_RAW dibutuhkan untuk monitor tipe ping tanpa menjalankan Node sebagai root.

Reverse Proxy Nginx dan SSL

Berikutnya, Uptime Kuma memakai WebSocket (Socket.IO). Karena itu, reverse proxy Nginx wajib meneruskan header Upgrade:

server {
    server_name monitor.contoh.com;
    location / {
        proxy_pass http://127.0.0.1:3001;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}
sudo systemctl daemon-reload && sudo systemctl enable --now uptime-kuma
sudo certbot --nginx -d monitor.contoh.com

Tips Menambahkan Monitor

Kemudian, buka https://monitor.contoh.com, buat akun admin, lalu tambahkan monitor. Beberapa tips dari pengalaman kami:

  • Pakai fitur Group untuk mengelompokkan monitor, misalnya “Produksi”, “Server Lokal”, dan “Infrastruktur”. Selanjutnya, nama group otomatis menjadi judul kelompok di dashboard NOC.
  • Untuk website, pilih tipe HTTP(s) – Keyword dan isi kata yang pasti ada di halaman. Sebaliknya, tipe HTTP biasa tetap hijau walau halaman menampilkan error aplikasi dengan status 200.
  • Untuk database atau layanan non-web (SQL Server, RADIUS, ACS), pakai tipe TCP Port.
  • Aktifkan Certificate Expiry Notification agar Anda diingatkan sebelum SSL habis.

Langkah 2: Pasang Beszel Hub

Beszel adalah alternatif ringan untuk Grafana + Prometheus. Bahkan, hub-nya hanya satu binary dengan database PocketBase di dalamnya.

sudo useradd -r -m -d /opt/beszel -s /usr/sbin/nologin beszel
cd /opt/beszel
curl -sL https://github.com/henrygd/beszel/releases/latest/download/beszel_linux_amd64.tar.gz \
  | sudo tar -xz beszel
sudo chown -R beszel:beszel /opt/beszel

Service Beszel Hub

Setelah binary siap, buat service /etc/systemd/system/beszel-hub.service:

[Unit]
Description=Beszel Hub
After=network-online.target
Wants=network-online.target

[Service]
User=beszel
Group=beszel
WorkingDirectory=/opt/beszel
Environment=APP_URL=https://beszel.contoh.com
ExecStart=/opt/beszel/beszel serve --http 127.0.0.1:8090
Restart=always
RestartSec=5
MemoryMax=256M
NoNewPrivileges=true

[Install]
WantedBy=multi-user.target

Buat vhost Nginx untuk beszel.contoh.com yang mengarah ke http://127.0.0.1:8090, dengan header WebSocket yang sama seperti Uptime Kuma (agent berkomunikasi lewat WebSocket), lalu jalankan certbot. Setelah itu buka panelnya dan buat akun admin.

Langkah 3: Pasang beszel-agent di Setiap Server

Pertama, di panel Beszel klik Add System. Kemudian, panel menampilkan public key milik hub dan token unik untuk server tersebut. Di server yang akan dipantau:

sudo useradd -r -M -s /usr/sbin/nologin beszel-agent
sudo mkdir -p /opt/beszel-agent && cd /opt/beszel-agent
curl -sL https://github.com/henrygd/beszel/releases/latest/download/beszel-agent_linux_amd64.tar.gz \
  | sudo tar -xz beszel-agent

Service Agent

Selanjutnya, buat service /etc/systemd/system/beszel-agent.service. Isi KEY dan TOKEN dari panel:

[Unit]
Description=Beszel Agent
After=network-online.target
Wants=network-online.target

[Service]
User=beszel-agent
Group=beszel-agent
Environment="KEY=ssh-ed25519 AAAA...PUBLIC_KEY_DARI_PANEL"
Environment="TOKEN=TOKEN_DARI_PANEL"
Environment="HUB_URL=https://beszel.contoh.com"
StateDirectory=beszel-agent
Environment="DATA_DIR=/var/lib/beszel-agent"
ExecStart=/opt/beszel-agent/beszel-agent
Restart=always
RestartSec=10
MemoryMax=128M
NoNewPrivileges=true
ProtectSystem=full

[Install]
WantedBy=multi-user.target
sudo chmod 600 /etc/systemd/system/beszel-agent.service   # token tidak terbaca user lain
sudo systemctl daemon-reload && sudo systemctl enable --now beszel-agent
sudo journalctl -u beszel-agent -n 5

Dengan HUB_URL, agent yang membuka koneksi WebSocket keluar ke hub. Akibatnya, Anda tidak perlu membuka port apa pun di server yang dipantau, dan cara ini tetap jalan untuk server di balik NAT atau CGNAT (misalnya server Proxmox di kantor).

beszel-agent di server Proxmox terhubung ke Beszel hub lewat WebSocket
Screenshot asli: agent di server Proxmox kami tersambung ke hub. Domain hub disensor.

Langkah 4: Buat Collector Python

Inilah inti dashboard NOC. Singkatnya, collector membaca dua sumber lalu menulis satu file data.json. Sebelum menulis kode, buat user khusus dengan role readonly lewat panel admin PocketBase di https://beszel.contoh.com/_/. Kemudian, supaya user itu bisa melihat semua server, set environment SHARE_ALL_SYSTEMS=true di service hub, atau tambahkan user tersebut ke setiap system. Dengan begitu, kalaupun password collector bocor, tidak ada pengaturan yang bisa diubah.

File Konfigurasi

Setelah itu, siapkan file konfigurasi /etc/noc/noc.json (permission 600):

{
  "beszel_url": "http://127.0.0.1:8090",
  "beszel_email": "noc@contoh.com",
  "beszel_password": "PASSWORD_USER_READONLY",
  "out": "/var/www/noc/data.json"
}

Kode collector cukup panjang, jadi kita bahas per bagian. Keempat bagian di bawah ini disimpan berurutan dalam satu file /opt/noc/collector.py.

Bagian 1: Import dan Konfigurasi

Pertama, bagian awal file berisi library bawaan Python dan lokasi file yang dipakai:

#!/usr/bin/env python3
# NOC collector: gabungkan Uptime Kuma (SQLite, read-only) + Beszel (API)
# menjadi satu data.json untuk dashboard. Jalan terus, tulis ulang tiap 30 detik.
import json, os, sqlite3, time, urllib.parse, urllib.request
from datetime import datetime, timezone

CONF = os.environ.get("NOC_CONF", "/etc/noc/noc.json")
KUMA_DB = "file:/opt/uptime-kuma/data/kuma.db?mode=ro"
INTERVAL = 30

Collector hanya memakai library standar Python, jadi tidak perlu pip install apa pun. Selain itu, NOC_CONF bisa diganti lewat environment saat pengujian.

Bagian 2: Membaca Data Uptime Kuma

Kemudian, fungsi kuma() membaca database Uptime Kuma secara langsung:

def kuma():
    c = sqlite3.connect(KUMA_DB, uri=True, timeout=10)
    c.row_factory = sqlite3.Row
    q = lambda sql, *a: c.execute(sql, a).fetchall()

    mons = {r["id"]: dict(r) for r in q("select id, name, type, parent, active from monitor")}
    last = {r["monitor_id"]: r for r in q(
        "select h.monitor_id, h.status, h.ping, h.msg from heartbeat h "
        "join (select monitor_id, max(id) mid from heartbeat group by monitor_id) x on h.id = x.mid")}
    upt = {r["monitor_id"]: r for r in q(
        "select monitor_id, sum(up) up, sum(down) down from stat_hourly "
        "where timestamp >= ? group by monitor_id", int(time.time()) - 86400)}

    sites = []
    for mid, m in mons.items():
        if m["type"] == "group" or not m["active"]:
            continue
        h, u = last.get(mid), upt.get(mid)
        total = (u["up"] + u["down"]) if u else 0
        sites.append({
            "name": m["name"],
            "group": mons.get(m["parent"], {}).get("name", "Lainnya"),
            "status": h["status"] if h else None,      # 0 down, 1 up, 2 pending, 3 maintenance
            "ping": round(h["ping"]) if h and h["ping"] is not None else None,
            "msg": (h["msg"] or "")[:120] if h else "",
            "uptime24": round(100.0 * u["up"] / total, 2) if total else None,
        })
    c.close()
    return sites
  • ?mode=ro membuka database dalam mode read-only. Akibatnya, collector tidak akan pernah mengunci atau merusak database yang sedang dipakai Uptime Kuma.
  • Query heartbeat mengambil hasil cek terakhir tiap monitor. Artinya, status 0 berarti down, 1 up, 2 pending, dan 3 maintenance.
  • Tabel stat_hourly (Uptime Kuma 2.x) berisi rekap per jam. Dengan begitu, uptime 24 jam bisa dihitung tanpa membaca ribuan baris heartbeat.

Bagian 3: Membaca Data Beszel

Selanjutnya, fungsi beszel() mengambil metrik server lewat API:

def beszel(conf):
    def req(path, token=None, body=None):
        r = urllib.request.Request(conf["beszel_url"] + path,
                                   data=json.dumps(body).encode() if body else None,
                                   headers={"Content-Type": "application/json",
                                            **({"Authorization": token} if token else {})})
        return json.loads(urllib.request.urlopen(r, timeout=15).read())

    token = req("/api/collections/users/auth-with-password",
                body={"identity": conf["beszel_email"], "password": conf["beszel_password"]})["token"]
    servers = []
    for s in req("/api/collections/systems/records?perPage=50&sort=name", token)["items"]:
        f = urllib.parse.quote("system='%s' && type='1m'" % s["id"])
        stats = req("/api/collections/system_stats/records?perPage=60&sort=-created&fields=stats&filter=" + f,
                    token)["items"][::-1]
        i = s.get("info") or {}
        servers.append({
            "name": s["name"], "status": s["status"],
            "cpu": i.get("cpu"), "mem": i.get("mp"), "disk": i.get("dp"),
            "spark_cpu": [round(x["stats"].get("cpu", 0), 1) for x in stats],
        })
    return servers

auth-with-password adalah endpoint login PocketBase. Kemudian, token yang didapat dipakai untuk membaca daftar server dan 60 data per menit terakhir sebagai grafik CPU (sparkline).

Bagian 4: Menggabungkan dan Menulis data.json

Terakhir, fungsi main() menjalankan keduanya setiap 30 detik:

def main():
    conf = json.load(open(CONF))
    out = conf["out"]
    while True:
        data = {"generated": datetime.now(timezone.utc).isoformat(timespec="seconds"), "errors": []}
        try:
            data["sites"] = kuma()
        except Exception as e:
            data["errors"].append("kuma: %s" % e)
        try:
            data["servers"] = beszel(conf)
        except Exception as e:
            data["errors"].append("beszel: %s" % e)
        tmp = out + ".tmp"
        with open(tmp, "w") as f:
            json.dump(data, f, separators=(",", ":"))
        os.chmod(tmp, 0o644)
        os.replace(tmp, out)          # ganti atomik: browser tidak pernah membaca file setengah jadi
        if os.environ.get("NOC_ONCE"):
            break
        time.sleep(INTERVAL)


if __name__ == "__main__":
    main()
  • os.replace menulis ke file sementara lalu menggantinya sekaligus. Oleh karena itu, halaman NOC tidak pernah membaca JSON yang baru setengah tertulis.
  • Kalau salah satu sumber error, collector tetap jalan dan mencatatnya di errors. Dengan demikian, dashboard tidak ikut blank hanya karena satu sumber bermasalah.

Menjalankan Collector sebagai Service

Setelah keempat bagian tersimpan, uji sekali jalan. Kemudian, jadikan service:

sudo mkdir -p /var/www/noc /opt/noc
sudo NOC_ONCE=1 python3 /opt/noc/collector.py
jq '.errors, (.sites|length), (.servers|length)' /var/www/noc/data.json
# /etc/systemd/system/noc-collector.service
[Unit]
Description=NOC collector (Uptime Kuma + Beszel -> data.json)
After=network-online.target uptime-kuma.service beszel-hub.service

[Service]
ExecStart=/usr/bin/python3 /opt/noc/collector.py
Restart=always
RestartSec=15
MemoryMax=96M
NoNewPrivileges=true
ProtectSystem=full
ProtectHome=yes

[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload && sudo systemctl enable --now noc-collector
Status service Uptime Kuma, Beszel hub, dan NOC collector di VPS monitoring
Screenshot asli dari VPS monitoring kami: ketiga service aktif, port hanya mendengarkan di localhost, dan data.json berisi 22 monitor serta 10 server.

Langkah 5: Buat Halaman Dashboard

Halaman NOC hanyalah satu file HTML tanpa framework. Selanjutnya, JavaScript membaca data.json setiap 30 detik lalu menyusun ulang tampilannya. Keempat bagian di bawah ini disimpan berurutan dalam satu file /var/www/noc/index.html.

Bagian 1: Tampilan (CSS)

Pertama, bagian <head> berisi warna dan tata letak layar NOC bertema gelap:

<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="robots" content="noindex, nofollow">
<title>NOC Dashboard</title>
<style>
  :root { --bg:#0d0f12; --card:#171a1f; --card2:#1f232a; --text:#eef1f5; --muted:#8a93a0;
          --good:#16a34a; --warn:#f5a524; --bad:#dc2626; --bar:#3b82f6; }
  * { box-sizing:border-box; margin:0; padding:0; }
  body { background:var(--bg); color:var(--text); font:15px/1.4 system-ui, "Segoe UI", sans-serif; padding:18px; }
  header { display:flex; justify-content:space-between; align-items:center; margin-bottom:14px; }
  h1 { font-size:22px; } #clock { font-size:22px; font-weight:600; }
  #banner { padding:10px 18px; border-radius:10px; font-weight:700; font-size:18px; background:var(--good); }
  #banner.bad { background:var(--bad); animation:pulse 1.4s infinite; }
  @keyframes pulse { 50% { opacity:.6; } }

  main { display:grid; grid-template-columns:1.1fr 1fr; gap:14px; }
  .panel { background:var(--card); border-radius:12px; padding:14px; }
  .panel h2 { font-size:15px; color:var(--muted); margin-bottom:10px; }
  .grp { color:var(--muted); font-size:12px; text-transform:uppercase; letter-spacing:.06em; margin:10px 0 6px; }

  .site { display:grid; grid-template-columns:auto 1fr auto auto; gap:10px; align-items:center;
          background:var(--card2); border-radius:8px; padding:7px 10px; margin-bottom:6px; }
  .site.down { outline:2px solid var(--bad); }
  .dot { width:11px; height:11px; border-radius:50%; background:var(--muted); }
  .dot.up { background:var(--good); } .dot.down { background:var(--bad); } .dot.pending { background:var(--warn); }
  .num { color:var(--muted); font-variant-numeric:tabular-nums; text-align:right; }

  .servers { display:grid; grid-template-columns:repeat(2, minmax(0,1fr)); gap:10px; }
  .srv { background:var(--card2); border-radius:8px; padding:10px; }
  .srv.down { outline:2px solid var(--bad); }
  .srv b { display:flex; justify-content:space-between; margin-bottom:6px; }

  .meter { display:grid; grid-template-columns:42px 1fr 46px; gap:8px; align-items:center; font-size:13px; }
  .bar { height:8px; background:#2a2f37; border-radius:9px; overflow:hidden; }
  .bar span { display:block; height:100%; background:var(--bar); }
  .bar span.warn { background:var(--warn); } .bar span.bad { background:var(--bad); }
  svg { width:100%; height:34px; margin-top:6px; }
</style>
</head>

Bagian 2: Struktur Halaman

Kemudian, kerangka halamannya. Isinya sengaja kosong karena nanti diisi JavaScript:

<body>
<header>
  <h1>NOC Dashboard</h1>
  <div id="banner">Memuat data…</div>
  <div id="clock">--:--:--</div>
</header>
<main>
  <section class="panel"><h2>Website &amp; layanan (Uptime Kuma)</h2><div id="sites"></div></section>
  <section class="panel"><h2>Server (Beszel)</h2><div class="servers" id="servers"></div></section>
</main>

Bagian 3: Fungsi Bantu

Selanjutnya, beberapa fungsi kecil untuk keamanan teks, bar persentase, dan grafik:

<script>
  var LIMIT = { cpu: 95, mem: 90, disk: 85 };
  var ENT = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' };
  function esc(s) { return String(s == null ? '' : s).replace(/[&<>"']/g, function (ch) { return ENT[ch]; }); }
  function cls(v, t) { return v == null ? '' : v >= t ? 'bad' : v >= t - 10 ? 'warn' : ''; }

  function meter(label, v, t) {
    var p = v == null ? 0 : Math.min(100, v);
    return '<div class="meter"><span>' + label + '</span><div class="bar"><span class="' + cls(v, t) +
      '" style="width:' + p + '%"></span></div><span class="num">' + (v == null ? '–' : v.toFixed(0) + '%') + '</span></div>';
  }

  function spark(vals) {
    if (!vals || vals.length < 2) return '';
    var max = Math.max(10, Math.max.apply(null, vals)), step = 100 / (vals.length - 1);
    var pts = vals.map(function (v, i) { return (i * step).toFixed(1) + ',' + (30 - v / max * 28).toFixed(1); }).join(' ');
    return '<svg viewBox="0 0 100 30" preserveAspectRatio="none"><polyline points="' + pts +
      '" fill="none" stroke="#3b82f6" stroke-width="1.5" vector-effect="non-scaling-stroke"/></svg>';
  }

  var DOT = { 0: 'down', 1: 'up', 2: 'pending', 3: 'pending' };
  • esc() meng-escape semua teks dari data sebelum dimasukkan ke innerHTML. Sebab, nama monitor berasal dari input admin, jadi tetap harus diperlakukan sebagai data, bukan HTML (mencegah XSS).
  • Sparkline digambar dengan SVG polyline biasa, tanpa library chart. Akibatnya, halaman tetap ringan untuk TV box dengan CPU lemah.
  • Batas warna (LIMIT) sengaja disamakan dengan aturan alert di Beszel. Dengan begitu, warna merah di layar artinya sama dengan notifikasi yang masuk ke Telegram.

Bagian 4: Menampilkan dan Memuat Data

Terakhir, fungsi render() menyusun tampilan, sedangkan load() mengambil data terbaru:

  function render(d) {
    var sites = d.sites || [], servers = d.servers || [], groups = {};
    sites.forEach(function (s) { (groups[s.group] = groups[s.group] || []).push(s); });
    document.getElementById('sites').innerHTML = Object.keys(groups).map(function (g) {
      return '<div class="grp">' + esc(g) + '</div>' + groups[g].map(function (s) {
        return '<div class="site' + (s.status === 0 ? ' down' : '') + '"><span class="dot ' + (DOT[s.status] || '') + '"></span>' +
          '<span>' + esc(s.name) + '</span><span class="num">' + (s.ping == null ? '' : s.ping + ' ms') + '</span>' +
          '<span class="num">' + (s.uptime24 == null ? '–' : s.uptime24 + '%') + '</span></div>';
      }).join('');
    }).join('');

    document.getElementById('servers').innerHTML = servers.map(function (s) {
      var up = s.status === 'up';
      return '<div class="srv' + (up ? '' : ' down') + '"><b><span>' + esc(s.name) + '</span><span class="dot ' +
        (up ? 'up' : 'down') + '"></span></b>' + meter('CPU', s.cpu, LIMIT.cpu) + meter('RAM', s.mem, LIMIT.mem) +
        meter('Disk', s.disk, LIMIT.disk) + spark(s.spark_cpu) + '</div>';
    }).join('');

    var down = sites.filter(function (s) { return s.status === 0; }).map(function (s) { return s.name; })
      .concat(servers.filter(function (s) { return s.status !== 'up'; }).map(function (s) { return 'server ' + s.name; }));
    var b = document.getElementById('banner');
    b.className = down.length ? 'bad' : '';
    b.textContent = down.length ? down.length + ' GANGGUAN: ' + down.join(', ') : '✓ SEMUA NORMAL';
  }


  function load() {
    fetch('data.json?t=' + Date.now(), { cache: 'no-store' })
      .then(function (r) { return r.json(); }).then(render)
      .catch(function () { document.getElementById('banner').textContent = 'Data tidak bisa dimuat'; });
  }
  setInterval(function () { document.getElementById('clock').textContent = new Date().toLocaleTimeString('id-ID'); }, 1000);
  setInterval(load, 30000);
  load();
</script>
</body>
</html>

Perhatikan ?t=' + Date.now() dan cache: 'no-store'. Keduanya memastikan browser selalu mengambil data terbaru, bukan dari cache.

Hasil Dashboard Versi Tutorial

Setelah keempat bagian disimpan, buka halaman /noc/ di browser. Hasilnya kurang lebih seperti ini:

Tampilan dashboard monitoring server versi tutorial dengan data asli
Kode di atas dijalankan dengan data asli dari server kami. Nama monitor yang memuat domain atau IP address disensor.

Versi di atas sengaja dibuat ringkas agar mudah dipahami. Sementara itu, dashboard produksi kami (gambar paling atas) menambahkan kartu KPI, daftar kejadian 24 jam terakhir, masa berlaku SSL terdekat, dan peringatan jika data.json tidak diperbarui lebih dari 2 menit. Selain itu, semua fitur tersebut bisa ditambahkan dengan pola yang sama.

Langkah 6: Amankan Halaman NOC dengan Key

Layar TV tidak bisa mengetik password setiap pagi. Namun, halaman NOC juga tidak boleh terbuka untuk siapa saja karena berisi daftar server Anda. Oleh karena itu, akses pertama memakai ?key=, lalu Nginx memasang cookie sehingga URL berikutnya tidak perlu key lagi. Pertama, buat key acak dengan openssl rand -hex 16, lalu tambahkan ke /etc/nginx/conf.d/noc.conf:

map $cookie_noc $noc_cookie_ok { "GANTI_DENGAN_KEY_ACAK" 1; default 0; }
map $arg_key    $noc_arg_ok    { "GANTI_DENGAN_KEY_ACAK" 1; default 0; }

Konfigurasi Lokasi /noc/

Kemudian, di blok server milik monitor.contoh.com, di atas location / Uptime Kuma:

location = /noc { return 301 /noc/$is_args$args; }
location ^~ /noc/ {
    set $noc_ok "${noc_cookie_ok}${noc_arg_ok}";
    if ($noc_ok = "00") { return 403; }
    alias /var/www/noc/;
    add_header Set-Cookie "noc=GANTI_DENGAN_KEY_ACAK; Path=/noc/; Max-Age=31536000; Secure; HttpOnly; SameSite=Lax" always;
    add_header Cache-Control "no-store" always;
}

Dengan demikian, https://monitor.contoh.com/noc/?key=GANTI_DENGAN_KEY_ACAK bisa dibuka, sedangkan tanpa key hasilnya 403. Selain itu, perhatikan modifier ^~: ini membuat Nginx berhenti mencari location lain, sehingga request /noc/ tidak ikut diteruskan ke Uptime Kuma.

Langkah 7: Notifikasi Telegram

Dashboard memang bagus untuk dilihat. Namun, Anda tidak menatap layar 24 jam. Karena itu, untuk alert pakai fitur bawaan masing-masing aplikasi:

  • Uptime Kuma: Settings → Notifications → Setup Notification, pilih Telegram, isi bot token (dari @BotFather) dan chat ID. Centang Default enabled agar monitor baru otomatis ikut.
  • Beszel: Settings → Notifications, isi URL Shoutrrr telegram://BOT_TOKEN@telegram?chats=CHAT_ID, lalu atur alert per server: CPU di atas 95% selama 15 menit, RAM di atas 90%, disk di atas 85%, dan status down.

Selain itu, untuk server di jaringan yang kurang stabil (misalnya kantor dengan internet satelit), naikkan durasi alert status menjadi 10 menit agar putus sebentar tidak memicu alarm palsu.

Bonus: Tampilkan di Layar TV (Mode Kiosk)

Cara paling hemat adalah memakai perangkat yang sudah ada. Misalnya, PC mini, TV box Linux, atau bahkan server Proxmox yang punya monitor. Kemudian, pasang cage (Wayland compositor khusus satu aplikasi) dan Chromium, lalu jalankan otomatis saat boot:

sudo apt install -y --no-install-recommends cage chromium fonts-noto-color-emoji
sudo useradd -m -s /usr/sbin/nologin -G video,render,input kiosk
sudo mkdir -p /etc/chromium/policies/managed
echo '{"TranslateEnabled": false, "PasswordManagerEnabled": false}' | sudo tee /etc/chromium/policies/managed/kiosk.json

Service Kiosk

Selanjutnya, buat service yang menjalankan Chromium di layar utama (tty1):

# /etc/systemd/system/noc-kiosk.service
[Unit]
Description=NOC kiosk
After=systemd-user-sessions.service network-online.target
Wants=network-online.target
Conflicts=getty@tty1.service

[Service]
User=kiosk
PAMName=login
TTYPath=/dev/tty1
TTYReset=yes
TTYVHangup=yes
StandardInput=tty-fail
Environment=XDG_SESSION_TYPE=wayland
ExecStart=/usr/bin/cage -d -s -- /usr/bin/chromium --kiosk --ozone-platform=wayland --noerrdialogs --no-first-run "https://monitor.contoh.com/noc/?key=GANTI_DENGAN_KEY_ACAK"
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target

Terakhir, aktifkan dengan systemctl enable --now noc-kiosk. Akibatnya, Chromium tampil layar penuh tanpa menu, dan otomatis menyala lagi kalau crash. Policy TranslateEnabled: false menghilangkan popup “Terjemahkan halaman ini?” yang sering muncul di layar kiosk. Sebaliknya, mematikannya lewat flag command line saja ternyata tidak cukup.

Dashboard monitoring server NOC tampil di layar TV mode kiosk
Ilustrasi pemasangan di dinding kantor. Isi layar adalah screenshot asli NOC kami yang sudah disensor.

Contoh Kasus Nyata

Contoh Kasus 1: Website Klien Mati Jam 2 Pagi

Sebuah aplikasi SaaS klien berhenti merespons karena PHP-FPM kehabisan worker. Akibatnya, Uptime Kuma mendeteksi keyword halaman login hilang, notifikasi Telegram masuk dalam 1 menit, dan pagi harinya layar NOC langsung menunjukkan riwayat kejadian beserta durasinya. Sebaliknya, tanpa monitoring, kasus seperti ini biasanya baru ketahuan setelah klien menelepon.

Contoh Kasus 2: Disk Penuh Pelan-Pelan

Salah satu server menyimpan backup harian dari server produksi. Setiap hari ukurannya naik sedikit, jadi tidak ada yang sadar. Namun, di dashboard NOC, bar disk server itu berubah kuning saat melewati 75% dan merah di 85%. Dengan begitu kami punya waktu berhari-hari untuk membersihkan backup lama atau menambah disk, jauh sebelum database berhenti menulis.

MasalahDideteksi olehTampil di NOC sebagai
Website down / errorUptime Kuma (HTTP keyword)Baris merah + banner “GANGGUAN”
Database tidak bisa diaksesUptime Kuma (TCP Port)Baris merah di grup Infrastruktur
Server mati / agent putusBeszel (status)Kartu server bergaris merah
Disk hampir penuhBeszel (disk %)Bar disk kuning lalu merah
CPU tinggi terus-menerusBeszel (CPU + sparkline)Grafik CPU naik dan bertahan

Kesalahan Umum

  • Monitoring dipasang di server yang dipantau. Akibatnya, saat server itu mati, monitoring ikut mati dan tidak ada yang mengirim alert. Karena itu, selalu pisahkan.
  • Uptime Kuma error “connection lost” di browser. Header Upgrade dan Connection "upgrade" belum ada di reverse proxy Nginx.
  • no such table: stat_hourly. Anda masih memakai Uptime Kuma 1.x. Upgrade ke 2.x, atau hitung uptime dari tabel heartbeat.
  • Collector error unable to open database file. User yang menjalankan collector tidak punya izin baca ke kuma.db, atau path-nya berbeda dengan instalasi Anda.
  • Daftar server kosong padahal login Beszel berhasil. User readonly belum diberi akses ke system. Set SHARE_ALL_SYSTEMS=true atau tambahkan user ke setiap system.
  • Dashboard menampilkan data lama. Lupa cache: 'no-store', atau ada cache Nginx/CDN di depan /noc/.
  • Halaman /noc/ malah membuka Uptime Kuma. Location /noc/ tidak memakai ^~, atau ditulis di bawah location yang lebih umum.
  • Agent tidak mau tersambung. Biasanya TOKEN salah salin, atau vhost Beszel tidak meneruskan WebSocket.
  • Layar kiosk memunculkan popup terjemahan. Pakai policy TranslateEnabled: false, bukan sekadar flag --disable-features.
  • Key NOC dibagikan di grup chat. Siapa pun yang memegang link bisa melihat daftar server Anda. Kalau bocor, ganti key di config Nginx lalu reload.

Kesimpulan

Dengan Uptime Kuma, Beszel, sekitar 100 baris Python, dan satu file HTML, Anda sudah punya dashboard monitoring server versi sendiri yang ringan dan gratis. Uptime Kuma mengecek dari luar seperti pengguna sungguhan, Beszel membaca kondisi dari dalam server, dan collector menyatukan keduanya ke satu layar NOC yang bisa dipasang di TV kantor. Selain itu, dengan notifikasi Telegram, tim Anda akan tahu ada masalah lebih dulu daripada pelanggan.

Oleh karena itu, mulailah dari yang sederhana seperti di tutorial ini. Setelah berjalan, tambahkan kartu KPI, daftar kejadian, atau data keamanan dari CrowdSec ke data.json. Strukturnya sudah siap untuk dikembangkan.

Baca Juga

Referensi: Uptime Kuma Wiki, Dokumentasi Beszel, PocketBase Records API, Chrome Enterprise Policy List.

Ali Akbar

Software Developer yang fokus mengembangkan aplikasi berbasis Web dan Desktop. Senang mempelajari teknologi baru terutama di bidang web design dan web development.

View all posts by Ali Akbar →

Tinggalkan Balasan

Alamat email Anda tidak akan dipublikasikan. Ruas yang wajib ditandai *