Your VPS is down. You don’t know it yet because nobody’s pinged you. The customer who emailed at 3 AM gets the “our server is unavailable” auto-responder, but it’s actually a real outage. By the time you wake up and check, three more customers have churned. You needed a status page that posts uptime/downtime in real time so you’d have noticed at 3:01 AM, and so customers can self-serve the “is your service down?” question instead of emailing you.
Three credible options for self-hosting a status page in 2026: Uptime Kuma, Statping (-NG), and the “just-write-it-yourself” bash + GitHub Pages combo. I’ve run the first two in production and the third for a personal side-project. Here’s the honest comparison.
Uptime Kuma — the good default
Uptime Kuma is the obvious choice for most people. Single docker-compose, beautiful UI, supports HTTP / TCP / Ping / DNS / push / Docker container / database / Steam / GameDig probes. Notification integrations: Slack, Discord, Telegram, generic webhooks, email, Gotify, Apprise — ~80 backends. Status pages are first-class.
- What’s good. Looks great out of the box. Set up takes 5 minutes. Each monitor stores history; you can drill into any incident. Public status pages are clean and embeddable. Active development, healthy community.
- What’s annoying. SQLite-based, single-node. If your homelab box dies, the status page goes with it — precisely when you need it. Not horizontally scalable. The mobile UI is OK but not great.
- When it fits. Personal infra, small business, anything where one well-configured node is enough.
# /opt/uptime-kuma/docker-compose.yml
services:
kuma:
image: louislam/uptime-kuma:latest
restart: unless-stopped
ports:
- "127.0.0.1:3001:3001"
volumes:
- ./data:/app/data
# In Caddy:
# status.example.com {
# reverse_proxy 127.0.0.1:3001
# }First-run wizard sets the admin user. Add monitors. Settings → Status Pages → New creates a public page at status.example.com/status/main.
Statping-NG — the “runs on Linux/Windows/Mac native” one
Statping is the older, statip-tradition tool. Statping-NG is a community fork that’s still maintained. Single Go binary, Postgres / MySQL / SQLite backend, public status page bundled in.
- What’s good. Single binary install on bare metal. Works without Docker. Postgres backend means you can put the data on durable storage independent of the runtime host. Built-in plugins for Slack, email, AWS SNS, Twilio.
- What’s annoying. The UI is dated compared to Kuma. Status page customization is more limited. Probe types are fewer.
- When it fits. You’re allergic to Docker, you want a single Go binary, you have an existing Postgres you want to point at.
# Install (Linux amd64)
wget https://github.com/statping-ng/statping-ng/releases/latest/download/statping-linux-amd64.tar.gz
tar xzvf statping-linux-amd64.tar.gz
sudo mv statping /usr/local/bin/
# Run with sqlite (good for testing):
statping --location=/opt/statping --port=8080The hand-rolled bash + GitHub Pages combo
The third option is the one nobody talks about: write 50 lines of bash that probe your endpoints, push the results as JSON to a GitHub Pages site, and let the static-site renderer turn JSON into a status page. Crucially, the page is hosted on GitHub’s CDN — not on your own infra. So when your VPS goes down, the status page still works.
- What’s good. Trivial. The page is hosted on GitHub Pages, so it survives every outage of your own infra. Total cost: $0.
- What’s annoying. No notifications out of the box (you’d have to add them). No history beyond what’s in your git repo. UI is whatever you build.
- When it fits. Side projects, hobby infra, when you genuinely want the status page to outlast everything else.
#!/usr/bin/env bash
# /opt/status-probe/probe.sh — runs on a different host than the things being probed
set -euo pipefail
cd /opt/status-probe/repo
declare -A endpoints=(
[api]="https://api.example.com/health"
[web]="https://www.example.com/"
[db]="https://db-health.example.com:8080"
)
ts=$(date -u +%Y-%m-%dT%H:%M:%SZ)
echo -n '{"updated":"'"$ts"'","services":[' > status.json.new
sep=""
for name in "${!endpoints[@]}"; do
url="${endpoints[$name]}"
status="up"
curl -sf --max-time 10 "$url" >/dev/null || status="down"
echo -n "$sep{\"name\":\"$name\",\"url\":\"$url\",\"status\":\"$status\",\"checked\":\"$ts\"}" \
>> status.json.new
sep=","
done
echo -n ']}' >> status.json.new
mv status.json.new status.json
git add status.json
git commit -m "status update $ts" || exit 0
git push origin mainThe repo’s GitHub Pages renders an index.html that fetches and displays status.json in JS:
<!-- index.html (in repo root) -->
<script>
fetch('status.json').then(r => r.json()).then(d => {
const html = d.services.map(s =>
`<tr><td>${s.name}</td><td class="${s.status}">${s.status}</td></tr>`
).join('');
document.getElementById('rows').innerHTML = html;
});
</script>Cron runs the probe every 60 seconds. GitHub Pages serves the HTML and JSON. Public URL is https://yourname.github.io/status-page/. Outages on your VPS don’t affect this page at all.
The decision rule
- You want notifications, history, drill-downs, and a polished UI — you’re OK with self-hosting. → Uptime Kuma. Genuinely the best choice for ~90% of people.
- You’re allergic to Docker, want a single binary, have a Postgres handy. → Statping-NG.
- The status page MUST survive when your infra goes down. → Hand-rolled + GitHub Pages. The whole point is host-elsewhere.
- You’re paying for a SaaS already (Pingdom, Better Uptime, etc.). → Use that. Self-hosting a status page that you have to keep up is its own infra burden.
One trap to avoid: do not host your status page on the same VPS as the things being probed. A status page that goes dark every time your real services do is worse than nothing. Either run Kuma/Statping on a separate cheap box (Hetzner CX22 in another region, $4/month), or use the GitHub Pages trick for outage independence.
Photo: Laptop with an analytics dashboard by Goumbik on Pexels.
