
When your game server or app goes down, the first alarm is often a wall of "is it down?" messages in Discord. A status page answers that question before anyone asks, but only if it keeps working while the thing it reports on is broken. This guide covers which tool to pick, where to host it, a setup with HTTPS and alerts, and how to keep it believable. Commands are for Ubuntu 24.04 and Debian 12.
Quick answer: For most small teams, run Uptime Kuma in Docker on a separate small VPS, with Caddy in front to get and renew the HTTPS certificate for a name like status.example.com. Uptime Kuma gives you monitors, status pages, incident posts and more than 90 notification services in a web interface. Pick Gatus if you would rather keep everything in one YAML file, or Upptime (GitHub Actions and Pages) or cState (a static site you update by hand) if you don't want to run a server for it. Whichever you choose, host it away from what it watches, check what your users actually use, and post incidents yourself.
Which Tool Fits
| Uptime Kuma | Gatus | Upptime | cState | |
|---|---|---|---|---|
| Runs on | Your VPS, in Docker | Your VPS, in Docker | GitHub Actions and Pages | Any static host |
| Configured in | A web interface | config.yaml | .upptimerc.yml in a repository | Markdown files |
| Checks | HTTP, keyword, TCP, ping, DNS, push, game servers | HTTP, TCP, UDP, ICMP, DNS, SSH, TLS, with conditions | HTTP, every 5 minutes at best | None |
| Incidents | Posted on the status page | announcements in the config | GitHub issues, opened and closed for you | Files you write |
Uptime Kuma is at version 2.5.8 (26 October 2026), and its README lists multiple status pages, each mappable to its own domain. Gatus v5.38.0 tests each endpoint against conditions such as [STATUS] == 200 or [RESPONSE_TIME] < 500, with alert providers from Discord and email to ntfy and PagerDuty. If you already run Uptime Kuma as a private monitor (our overview), the status page is the same install with a public face.
Host It Away From What It Watches
A status page earns its keep when everything else is down, so it must not share their failure. During the Amazon S3 outage in US-EAST-1 on 28 February 2017, AWS's own summary says it was "unable to update the individual services' status" on its Service Health Dashboard "because of a dependency the SHD administration console has on Amazon S3." It posted updates on Twitter and in a banner instead.
For a small setup, that means:
- Another machine, ideally at another provider or data center. A status page on the same VPS as your site goes dark at exactly the wrong moment.
- A hostname that doesn't depend on those servers. status.example.com works if your DNS is hosted elsewhere; GitHub goes further with a separate domain, githubstatus.com.
- No shared proxy in front. If the status page sits behind the same CDN as your site, a CDN problem takes out both.
If your apps run at home or with another host, a small Space-Node VPS in our EU data center keeps the status page off the machines it reports on. If they already run with us, put the status page with another provider or on GitHub Pages, so one data center problem can't take down both.
Set Up Uptime Kuma With Caddy
1. Point the name at the status VPS. Create an A record for status.example.com with the VPS's IPv4 address, plus an AAAA record if you use IPv6. Caddy's automatic HTTPS works once the records point at the server, ports 80 and 443 are reachable from outside, and its data directory is writable and persistent.
2. Install Docker Engine with the Compose plugin using Docker's instructions for Ubuntu or Debian.
3. Create the files. Make a folder and open a new Compose file in your editor:
sudo mkdir -p /opt/status/conf
cd /opt/status
sudo nano compose.yaml
Paste this:
services:
uptime-kuma:
image: louislam/uptime-kuma:2
restart: unless-stopped
volumes:
- ./data:/app/data
caddy:
image: caddy:2
restart: unless-stopped
ports:
- "80:80"
- "443:443"
- "443:443/udp"
volumes:
- ./conf:/etc/caddy
- caddy_data:/data
- caddy_config:/config
volumes:
caddy_data:
caddy_config:
Then create the Caddyfile with sudo nano conf/Caddyfile, using your own name:
status.example.com {
reverse_proxy uptime-kuma:3001
}
Uptime Kuma publishes no port: Compose puts both containers on one network where each is reachable by its service name, so outside the server, port 3001 is reachable only through Caddy. That also sidesteps a common trap, because ports that Docker publishes bypass UFW (see our Docker and UFW guide). Caddy passes the Host header and WebSockets through by default, and Uptime Kuma needs both. The Caddy image docs say to mount the conf folder rather than the Caddyfile itself and to persist /data, which holds certificates and private keys.
4. Start it and finish setup straight away.
sudo docker compose up -d
sudo docker compose logs -f caddy
When the log shows certificate obtained successfully, press Ctrl+C and open https://status.example.com. Do this right away: until an admin account exists, whoever opens the page first gets the setup screen. Choose SQLite (recommended for small deployments) and create your account. Then, in Settings:
- Security: turn on two-factor authentication under 2FA Settings.
- Reverse Proxy: set Trust Proxy to Yes, as the reverse proxy page suggests when Uptime Kuma is only reachable through a proxy.
5. Add monitors. Use Add New Monitor for each thing your users rely on:
- HTTP(s) Keyword for websites. It searches the HTML or JSON response for your word, case-sensitive, so a page that loads but shows an error still fails.
- TCP Port for services without a web page.
- GameDig or Steam Game Server for game servers. The GameDig game list includes Minecraft (Java and Bedrock), Valheim, Rust, Palworld and Enshrouded.
- Certificate Expiry Notification, a checkbox on HTTPS monitors, warns you before a certificate runs out.
New monitors check every 60 seconds with Retries at 0, so a single failed check marks the monitor down. "Keep It Honest" below explains why to raise that.
6. Add alerts. In a monitor, click Set Up Notification and choose a type. For Discord, the form's help text says to get the webhook URL from Server Settings > Integrations > View Webhooks > New Webhook. Tick Default enabled, which adds it to new monitors, and Apply on all existing monitors.
7. Build the status page. Open Status Pages, then New Status Page, and enter a name and a slug. Use Add Group and Add a monitor to choose what the public sees, add status.example.com under Domain Names in the sidebar, and click Save. Your domain now shows the status page at /, the dashboard stays at /dashboard, and the page also lives at /status/<slug>.
To update, run sudo docker compose pull and then sudo docker compose up -d --force-recreate in /opt/status, as the update page shows. Back up /opt/status/data: the v2 migration notes call backing up the data directory "currently the only supported backup method", and our Docker backup guide shows how to archive a bind mount and copy it off the server.
Or Use Gatus With One Config File
Gatus keeps every check, alert and message in config.yaml, so your status page can live in Git. Use the same setup, run sudo mkdir -p /opt/status/config, and put this service in compose.yaml in place of uptime-kuma:
gatus:
image: ghcr.io/twin/gatus:stable
restart: unless-stopped
volumes:
- ./config:/config
- ./data:/data
Change the Caddyfile line to reverse_proxy gatus:8080, since Gatus listens on port 8080 by default, and create config/config.yaml:
storage:
type: sqlite
path: /data/data.db
alerting:
discord:
webhook-url: "https://discord.com/api/webhooks/**********/**********"
endpoints:
- name: Website
group: Public
url: "https://www.example.com/"
interval: 60s
conditions:
- "[STATUS] == 200"
- "[BODY] == pat(*Sign in*)"
- "[CERTIFICATE_EXPIRATION] > 48h"
alerts:
- type: discord
send-on-resolved: true
- name: Minecraft
group: Game servers
url: "tcp://play.example.com:25565"
interval: 60s
conditions:
- "[CONNECTED] == true"
alerts:
- type: discord
send-on-resolved: true
Replace Sign in with text that only appears when your page works. From the Gatus README:
- Storage defaults to memory, and "the data will not survive a restart"; the
sqliteblock, as in the project's SQLite example, keeps history in./data. - Gatus reloads the config when the file changes, but may miss updates if you mount the file instead of its folder. It exits on an invalid new config by default, so check
sudo docker compose logs gatusafter each edit. - Alerts fire after 3 failures in a row (
failure-threshold) and resolve after 2 successes (success-threshold);send-on-resolvedis off by default. [CONNECTED] == trueonly proves that something accepts connections on the port.
For incidents, add announcements with an RFC3339 timestamp, a type (outage, warning, information, operational or none) and a message; archived: true moves old ones to Past Announcements at the bottom. A maintenance block (start, duration, timezone, every) mutes alerts during planned work.
announcements:
- timestamp: 2026-10-30T14:00:00Z
type: outage
message: "Minecraft server down after a crash. Next update 14:30 UTC."
Static Options: Upptime and cState
Upptime runs entirely on GitHub: a scheduled workflow checks your URLs every 5 minutes, opens an issue when one is down and closes it when it's back, and GitHub Pages serves the status site. For your own domain, set cname under status-website in .upptimerc.yml (configuration docs). The catch is timing: GitHub's workflow docs make 5 minutes the shortest schedule and warn that scheduled runs can be delayed under high load, such as at the start of every hour, and that queued jobs may be dropped. That makes Upptime a slow monitor, but a good second opinion that shares nothing with your servers.
cState is a Hugo site: each incident is a Markdown file in content/issues, and its README is blunt that it "cannot do automatic monitoring out of the box." Host it on Netlify or any static host; it says exactly what you write, nothing more.
Keep It Honest
A page that shows green during an outage costs more trust than no page at all:
- Check what users use. A ping proves the machine answers. A keyword or body check proves the page works, and a GameDig check proves the game server answers queries.
- Filter blips, not outages. Uptime Kuma's Retries field counts the retries "before the service is marked as down and a notification is sent". Set it to 1 or 2: with the default 60-second Heartbeat Retry Interval, a real outage shows a minute or two later, and one lost check no longer turns the page red. Gatus thresholds only delay the alert; each failed check still shows.
- Know the delays. Uptime Kuma's public page is cached, unlike the live dashboard. In 2.5.8 the page data, incidents included, is cached for 5 minutes and the heartbeat bars for 1 minute; an open page fetches new bars every 300 seconds by default (Refresh Interval, minimum 5) and shows new incidents only after a reload. Post an incident as soon as you know.
- Write incidents for people. Say what is affected, since when (in UTC), what you are doing and when the next update comes; our disaster recovery checklist covers that first half hour. Resolve or archive old incidents instead of deleting them, so the history stays visible.
- Mark planned work. During a window set with Schedule Maintenance, Uptime Kuma marks the affected monitors as under maintenance instead of down and sends no notifications for them. Gatus's
maintenanceblock mutes alerts, so pair it with an announcement. - Watch the watcher. Nothing can report its own outage. Point a check from somewhere else at the status page itself: an Upptime repository, or Gatus at another provider.
If It Still Fails
- The dashboard shows "Cannot connect to the socket server. Reconnecting..." The proxy isn't passing WebSockets; Caddy does by default. For nginx, the reverse proxy page adds
proxy_http_version 1.1;,proxy_set_header Upgrade $http_upgrade;,proxy_set_header Connection "upgrade";andproxy_set_header Host $host;to the location block, and our nginx reverse proxy guide covers the certificate. Behind Cloudflare, the same page says to enable WebSockets under Network. - Caddy logs
could not get certificate from issuerwithDNS problem: NXDOMAIN looking up A for status.example.com. Let's Encrypt's resolver says the name doesn't exist, so fix the record at the DNS provider your domain actually uses. Caddy retries with growing pauses of up to a day;sudo docker compose restart caddytries again now. - The domain opens the login page instead of the status page. The domain is missing under Domain Names, or a proxy in between drops the Host header; the status page notes say nginx and Apache must forward
HostorX-Forwarded-Host. - Uptime Kuma answers on http://your-ip:3001 from outside. Something publishes port 3001, which bypasses UFW. Remove the
ports:lines, or bind them to loopback as the README shows:127.0.0.1:3001:3001. - Every monitor turns red at once. Check the status VPS first: when its own network fails, all checks fail together. That is what retries and a second checker are for.
FAQ
Can the status page run on the same VPS as my website? It can, but then it goes down with that VPS, exactly when people look for it. Keep the public page elsewhere.
How often should checks run? Uptime Kuma and Gatus both default to 60 seconds. The Gatus author uses 5 minutes for complex checks and 30 seconds for simple checks that trigger alerts. Upptime can't go below 5 minutes.
Can it show whether my Minecraft server is up?
Yes. Uptime Kuma's GameDig monitor supports Minecraft Java and Bedrock, though GameDig's notes warn that many Minecraft servers don't return player data, so judge on whether the server answers. In Gatus, a tcp:// check on port 25565 shows whether the port accepts connections.
Is it free? The software is: Uptime Kuma, Upptime and cState are MIT-licensed and Gatus is Apache 2.0, so you pay only for the server. Upptime's author built it to be "completely free if you're already using GitHub", and cState's README says "completely free with Netlify".
Should the status page sit behind Cloudflare? It can, with WebSockets enabled for Uptime Kuma. But if your main site also runs through Cloudflare, a Cloudflare problem hides both the site and the page that should explain it. A DNS-only record for the status page avoids that shared dependency.
Sources
All opened on 31 October 2026:
- Uptime Kuma: README, compose.yaml, release 2.5.8, wiki: Reverse Proxy, Status Page, How to Update, Migration from v1 to v2
- Uptime Kuma 2.5.8 source: status page routes, status page handlers, status page model, monitor model, server.js, StatusPage.vue, EditMonitor.vue, Security settings, socket.js, English UI text, refresh interval migration
- Gatus: README, SQLite example, release v5.38.0
- Caddy: Automatic HTTPS, reverse_proxy, Docker image docs, image tags, CertMagic log messages
- Let's Encrypt: Boulder DNS error text
- Docker: install on Ubuntu and Debian, Compose networking, compose up, compose logs, compose pull, compose restart, Packet filtering and firewalls
- Upptime: README, configuration; GitHub: Events that trigger workflows
- cState: README
- GameDig: games list
- AWS: Summary of the Amazon S3 service disruption, 28 February 2017; GitHub: GitHub Status