Patching, backup & uptimebeginner7 min read

Building a public status page without Statuspage.io: self-hosted, in 30 minutes, on a different server than your product

A status page on the same server as the service it describes is down when you need it. This is the setup that works: Uptime Kuma on a separate, cheap VPS, checks from outside on HTTP, TLS and TCP, a public page on status.yourdomain.com with its own certificate, incident updates you post from your phone, and the DNS and cache details that decide whether the page stays reachable when the rest is on fire.

Contents
  1. The one rule that decides everything
  2. Step 1: install Uptime Kuma
  3. Step 2: make it public with your own domain and TLS
  4. Step 3: the checks — from outside, on what the customer sees
  5. Step 4: the public page
  6. Step 5: incidents — from your phone
  7. Step 6: notifications and the status page's own heartbeat
  8. Maintenance
  9. Pitfalls
  10. What you still don't have
  11. How monsys does it
  12. FAQ

During an outage customers do three things: they reload the page, they email support, and they look for a status page. If there isn't one, or if it's down with the rest, your support load doubles at the moment you need your hands to fix things. Statuspage.io and friends solve that for 29 to 99 dollars a month with your customer data in the US. This is the version on your own VPS, with the same features, for the price of the smallest server at your hoster.

The one rule that decides everything

The status page doesn't run on the infrastructure it monitors. Different hoster, or at least a different datacenter, a different DNS provider if you can, its own certificate. A status page showing "all OK" because it's down itself is worse than no status page.

Practically: a 1-vCPU VPS with 1 GB RAM at a second hoster (Hetzner if you're at OVH, or vice versa) costs 4 to 5 euros a month and runs this easily.

Step 1: install Uptime Kuma

Uptime Kuma is open source (MIT), has HTTP/TCP/DNS/ping/keyword checks, public status pages, incident messages and notifications to ntfy, email and about ninety other channels. One container:

# On the status VPS
sudo apt install -y docker.io docker-compose-v2 caddy
sudo install -d /opt/status && cd /opt/status
sudo tee docker-compose.yml >/dev/null <<'EOF'
services:
  kuma:
    image: louislam/uptime-kuma:1
    container_name: uptime-kuma
    restart: unless-stopped
    ports: ["127.0.0.1:3001:3001"]
    volumes:
      - ./data:/app/data
    logging:
      driver: json-file
      options: { max-size: "10m", max-file: "3" }
EOF
sudo docker compose up -d

Open http://<vps-ip>:3001 via an SSH tunnel (ssh -L 3001:127.0.0.1:3001 status-vps) and create the admin account. Do it now: the first visitor of a fresh install gets to choose the admin account.

Step 2: make it public with your own domain and TLS

# DNS: status.example.com → A record to the status VPS, TTL 300 (not 86400!)
sudo tee /etc/caddy/Caddyfile >/dev/null <<'EOF'
status.example.com {
    reverse_proxy 127.0.0.1:3001
    header {
        # No caching of the status page by intermediate proxies/CDNs
        Cache-Control "no-store"
        X-Frame-Options "SAMEORIGIN"
    }
    encode gzip
}
EOF
sudo systemctl reload caddy
curl -sI https://status.example.com | head -3      # 200, cert from Let's Encrypt

Caddy handles the certificate. Two details that matter during an outage:

  • DNS TTL of 300 seconds. If you ever have to replace the status VPS, you want to repoint the A record within five minutes, not within a day.
  • Cache-Control: no-store. A CDN or corporate proxy caching this morning's page shows "operational" while your incident has been running for two hours.

Step 3: the checks — from outside, on what the customer sees

In the Kuma UI, Add New Monitor. One check per service that does what a visitor does:

ServiceTypeSetting
Website / appHTTP(s) – KeywordURL of the login page, keyword Sign in (not just status 200: an error page returns 200 too)
APIHTTP(s) – Json Queryhttps://api.example.com/healthz, query $.status == "ok"
MailTCP Portmail.example.com:587 and :993
DNSDNSyour own domain against your own nameserver, expected record
TLS(part of the HTTP check)Certificate Expiry Notification on, 14 days

Interval 60 s, retries 2 (so down after 3 failed attempts = ~3 minutes; flap protection), Upside Down Mode off. Set the Accepted Status Codes in the check to 200-299 — not 200-399, otherwise a redirect to an error page counts as up.

For services behind a firewall that are only reachable internally, use a Push monitor: the internal host reports in every minute with a curl to Kuma, and Kuma alerts if the push stops. Kuma stays outside your network and you still see internal services.

# On the internal host, cron every minute:
* * * * * curl -fsS -m 10 "https://status.example.com/api/push/AbCdEf1234?status=up&msg=OK" >/dev/null

Step 4: the public page

Status Pages → New Status Page: slug main, title, and drag the monitors into groups ("Website", "API", "Email"). What you don't put on the public page:

  • The URLs and ports of your checks (Kuma doesn't show them, unless you give the monitor name a URL — call it "Customer portal", not "https://app.example.com/login").
  • Internal services (database, Redis, backup). The customer gains nothing and it's information for an attacker.
  • Response-time graphs if you don't want to explain them; "Show Certificate Expiry" is fine — it signals diligence.

Set Custom domain to status.example.com and a short Description with what the customer should do during an outage ("For urgent questions: support@example.com — don't use email through the platform during an outage").

Step 5: incidents — from your phone

A status page without incident messages only shows red and green. What a customer wants to read: what is wrong, who is working on it and when the next update comes. In Kuma: on the status page Create Incident, title, text, style (info/warning/danger). That works from the mobile browser.

Keep the format fixed, then an update takes 30 seconds:

[14:05] Investigating — The API has been responding slowly since 13:50. We're investigating the cause. Next update at 14:30.
[14:28] Identified — Cause: full disk on the database server. Cleanup in progress. Next update at 15:00.
[14:52] Resolved — Disk space restored at 14:45; API response times normal since 14:47. Post-mortem to follow within 2 working days.

Don't keep incidents only on the page. Copy the timeline to your incident register afterwards — that's NIS2 evidence item 12, and the "early warning within 24 hours" clock starts at the first line.

Step 6: notifications and the status page's own heartbeat

Settings → Notifications → ntfy: topic status, and attach it to all monitors (Apply on all existing monitors). That way you get the message before the customer sees it. See on-call without PagerDuty for the escalation.

And watch the watcher: a monitor at another place (your main server, or the free tier of an external service) checking https://status.example.com every 5 minutes. Kuma can't report itself if it's down.

# On your main server (which is NOT the status VPS): cron every 5 min
*/5 * * * * curl -fsS -m 10 -o /dev/null https://status.example.com || curl -s -H "Title: STATUS PAGE DOWN" -H "Priority: urgent" -d "status.example.com unreachable" https://ntfy.example.com/oncall

Maintenance

# Backup of the Kuma data (SQLite + config): daily, to another place
0 4 * * * root tar -C /opt/status -czf /var/backups/kuma-$(date +\%F).tgz data && find /var/backups -name 'kuma-*.tgz' -mtime +14 -delete
# Updates: Kuma via the image tag; the VPS via unattended-upgrades
cd /opt/status && sudo docker compose pull && sudo docker compose up -d

Pitfalls

  • Status page on the same server. Once more, because it's the mistake everyone makes once. "Another VM at the same hoster in the same rack" also counts as the same server when the hoster has a network outage.
  • Checking only HTTP 200. A maintenance page, a WAF block and an empty app all return 200. Keyword or JSON checks look at the content.
  • Too aggressive an interval. 20 s with 0 retries gives a page that flashes red on every network hiccup. 60 s with 2 retries is the sweet spot for a public page; your internal monitoring may be sharper.
  • Forgetting the page is public. Monitor names, incident texts and descriptions are readable by everyone, including competitors and attackers. No hostnames, no "database server 3 is full".
  • No owner for updates. An incident without an update in 45 minutes reads as "they don't know". Agree who keeps the page updated during an incident — not the person fixing it.

What you still don't have

  • The link to the cause. Kuma sees the API is slow; why (disk full, CPU, a deploy) is in your server monitoring. Two systems, two logins, in the middle of an incident.
  • Access per customer. One public page for everyone. An MSP wants a page per customer with only their services, possibly behind a password or token.
  • Evidence. The uptime percentages on the page are what Kuma measured; for an SLA report or a NIS2 file you want them signed, with the raw measurements attached.

How monsys does it

The uptime checks in monsys run from the hub — a different location than your servers — with HTTP, TLS and TCP checks, two consecutive failures before anything is called down, and certificate expiry alerts at 14 days. Every tenant publishes status pages under its own slug, public, via a secret link or with a password — so an MSP has a page per customer. A down check is a regular alert in the same pipeline as the agent metrics, so the status page and the cause (disk full on the database host) are in the same screen. The measurements land in the SLA engine and the signed monthly report.

FAQ

Which self-hosted status page is the easiest?

Uptime Kuma: one container, checks and status page in one, incident messages built in, actively maintained. Alternatives: Gatus (configuration as YAML, no UI for incidents), Cachet (status page only, checks separate), Statping-ng.

Does the status page really need to be at a different hoster?

Yes, if you want it to work during an outage at your hoster — and that's exactly when you need it. At minimum a different datacenter; ideally a different hoster and a different DNS provider for the status subdomain.

How do I get the status page on my own domain?

An A record status.example.com to the status VPS (TTL 300), Caddy or nginx as reverse proxy with automatic Let's Encrypt certificate, and in Kuma set the custom domain on the status page. Don't reuse a wildcard certificate from your main domain: if that expires or is compromised, you want the status page independent of it.

Written by the monsys team — sysadmins who do this every day.

Done it by hand? Let monsys keep it running.

Everything in this guide runs in monsys as a continuous check, with history, alerts and audit evidence. 5 servers free, EU-hosted in Belgium, installed in 60 seconds.