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.

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
| Komponen | Fungsi | Cara mengambil data |
|---|---|---|
| Uptime Kuma | Cek website, API, port TCP, ping, dan masa berlaku SSL dari luar | Collector membaca SQLite kuma.db (read-only) |
| Beszel Hub | Menerima metrik CPU, RAM, disk, dan network dari agent | Collector memanggil REST API PocketBase |
| beszel-agent | Program kecil di tiap server yang mengirim metrik ke hub | Koneksi WebSocket keluar ke hub |
| collector.py | Menggabungkan dua sumber menjadi data.json setiap 30 detik | Python 3 bawaan, tanpa library tambahan |
| Halaman NOC | HTML + JavaScript yang membaca data.json | Disajikan Nginx di /noc/ |

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
| Kebutuhan | Contoh di tutorial |
|---|---|
| VPS Ubuntu 24.04 (terpisah dari server yang dipantau) | IP 203.0.113.20 |
| Subdomain untuk panel | monitor.contoh.com (Uptime Kuma + NOC), beszel.contoh.com |
| Nginx + Certbot | Reverse proxy dan SSL Let’s Encrypt |
| Node.js 20 ke atas | Untuk 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).

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=romembuka database dalam mode read-only. Akibatnya, collector tidak akan pernah mengunci atau merusak database yang sedang dipakai Uptime Kuma.- Query
heartbeatmengambil hasil cek terakhir tiap monitor. Artinya, status0berarti down,1up,2pending, dan3maintenance. - 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.replacemenulis 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

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 & 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 = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' };
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 keinnerHTML. Sebab, nama monitor berasal dari input admin, jadi tetap harus diperlakukan sebagai data, bukan HTML (mencegah XSS).- Sparkline digambar dengan SVG
polylinebiasa, 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:

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.

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.
| Masalah | Dideteksi oleh | Tampil di NOC sebagai |
|---|---|---|
| Website down / error | Uptime Kuma (HTTP keyword) | Baris merah + banner “GANGGUAN” |
| Database tidak bisa diakses | Uptime Kuma (TCP Port) | Baris merah di grup Infrastruktur |
| Server mati / agent putus | Beszel (status) | Kartu server bergaris merah |
| Disk hampir penuh | Beszel (disk %) | Bar disk kuning lalu merah |
| CPU tinggi terus-menerus | Beszel (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
UpgradedanConnection "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 tabelheartbeat.- Collector error
unable to open database file. User yang menjalankan collector tidak punya izin baca kekuma.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=trueatau 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
TOKENsalah 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
- Cara Akses Proxmox dari Internet Tanpa IP Publik (WireGuard + VPS)
- Cara Mencegah Serangan XSS dan CSRF di PHP
- Cara Memasang SSL dan HTTPS di WordPress
Referensi: Uptime Kuma Wiki, Dokumentasi Beszel, PocketBase Records API, Chrome Enterprise Policy List.

