10
Check types per service
90 d
Check history retained
10 s
Fastest poll interval
∞
Branded dashboards per install
In this case study
The problem
I operate infrastructure for myself and for other people: web servers, mail, DNS, WireGuard gateways, Raspberry Pis, TP-Link Omada networks and point-of-sale hardware. Every audience wanted its own status page with its own name, colours and domain, and none of them should see each other’s servers.
Hosted status-page products price per page or per seat, hold your uptime data on their servers, and rarely understand network gear or POS devices. Running one open-source monitor per client multiplies upgrades, backups and logins.
Goal: a single self-hosted deployment that serves unlimited branded status pages, understands the protocols and devices I actually run, and communicates incidents to customers automatically.
Architecture
The whole platform is one Node.js process in front of MariaDB, shipped as a container. Keeping it to a single service was deliberate: it is what makes upgrades a two-command affair and backups a single volume.
Poller
Runs every service’s checks on its own interval, records latency and outcome, and drives incident state transitions.
Express app
Admin console, public dashboards, JSON feeds, SSE live state, SVG badges and health endpoints from one server.
MariaDB
Services, checks, 90-day history, 180-cycle heartbeats, incidents, maintenance, users, audit and API keys.
Reverse proxy
Caddy terminates TLS. The app picks the dashboard from the Host header, so a custom domain is a DNS change.
Public pages live at /dashboard/<slug> and can be mapped to a custom domain. Each ships an installable web-app manifest that is intentionally network-only: a status page must never show stale cached state.
The check engine
A service can run several checks in the same poll cycle, each with a polling interval from 10 to 3,600 seconds and a failure threshold of 1 to 10 consecutive misses. Latency-reporting checks feed a 24-hour response-time chart, so degradation shows up before downtime does.
ICMP ping
Reachability and round-trip latency.
TCP port
Connection and connect time.
UDP port
Reachability for services like WireGuard.
HTTP / HTTPS
Status, response time, required or forbidden body text, TLS inspection.
DNS record
A, AAAA, CNAME, MX, TXT, NS with expected values.
TLS certificate
Expiry with a configurable warning threshold.
Omada
Gateway/WAN, LTE and AP/switch health via Open API v6.
UniFi
Gateway, AP/switch, WAN subsystem and minimum client count.
Square POS
Location or device availability with delayed outage confirmation.
Script
Admin-only command check; exit code decides health.
The Square check is a good example of learning from real alerts: POS devices briefly drop off Wi-Fi all the time, so outages are confirmed after a delay instead of paging on the first miss. Controller checks reject loopback, private and link-local targets by default, and an allow-list can restrict controller hostnames further.
Tenancy and access control
Dashboards are the tenancy boundary. A service can appear on several dashboards; a viewer account is granted specific dashboards and only ever sees, edits or receives alerts for those.
| Capability | Administrator | Viewer |
|---|---|---|
| Browse management data | All dashboards and services | Assigned dashboards only |
| Add or edit services | Yes | Within assigned dashboards |
| Delete services | Yes | No |
| Dashboard branding and settings | Full, including service assignment | Branding only |
| Alert channels | Global or per dashboard | Assigned-dashboard scope |
| Omada, UniFi, Square resources | All | Scoped to assigned dashboards |
| Users, API keys, audit, system logs | Yes | No |
Public status pages are visitor-facing by design. Viewer grants control what a user can manage, not who can read the public route; anonymous and viewer feeds omit private incidents and internal infrastructure details.
Incidents, maintenance and alerts
Incidents open and close themselves
Probe transitions automatically open and resolve incidents. Operators then refine what customers see: a custom title, minor, major or critical impact, public or private visibility, and investigating, identified, monitoring and resolved updates on a timeline.
Maintenance suppresses noise, not monitoring
A maintenance window can cover many services. The app stores one window per affected service, groups matching windows on public pages, and suppresses outbound status-change alerts while a service is in maintenance. Checks and history keep running so the record stays complete.
Alerts where the team already is
Channels can be global or scoped to a single dashboard, with Generic JSON, Discord, Slack and email payloads. URL detection also covers Microsoft Teams, Telegram, Pushover and ntfy. Public visitors can subscribe by email to down and recovery notices, and operators get weekly email reports.
Decisions that shaped the product
Removed a feature
High availability was cut in v3.4.0. Bidirectional MariaDB replication, a promote webhook, a split-brain guard and a Cloudflare Load Balancer kept two monitors alive, and the operational cost was out of proportion for a self-hosted monitor. Simplifying back to a single server changed no configuration and needed no downtime to upgrade.
Security hardening
API keys are stored as HMAC-SHA256 with the session secret as the pepper, key names are HTML-escaped, and the public subscribe endpoint was rewritten to be ReDoS-safe after a review of regex-driven input handling.
Observability first
Health is a first-class endpoint. /healthz answers liveness plus database connectivity for Docker; /health reports version, uptime, last poll age and service count; ?strict=1 additionally requires at least one configured service, which makes it a safe target for an external uptime monitor or load balancer.
Licensing
Source-available rather than open source. Personal and internal non-commercial use is free; modification, redistribution and commercial use need written permission. The code is fully readable, which matters for something that holds your infrastructure map.
Deployment, security and operations
The published image applegater/status-server runs as the non-root node user on linux/amd64 and linux/arm64, listens on port 3000, needs only NET_RAW for ICMP, and upgrades its own schema at start-up. Production refuses to start with the fallback session secret or a missing database password.
git clone https://github.com/X4Applegate/status-server.git
cd status-server
cp docker-compose.example.yml docker-compose.yml
# set SESSION_SECRET, DB_PASSWORD, EXTERNAL_URL, TZ
docker compose pull && docker compose up -d
curl --fail http://localhost:3000/healthz
Upgrades are a backup, a pull and a recreate of the app service. The session secret must stay stable across upgrades because it also peppers API-key hashes. In front of it, Caddy needs two lines per hostname, and each custom dashboard domain is just another site block pointing at the same container.
Richard Applegate