Self-Hosted Status Page on a VPS: Uptime Kuma or Gatus

Published on

Run a status page that stays up when your servers go down: Uptime Kuma or Gatus on a separate VPS, HTTPS through Caddy, alerts and honest incident posts.

VPS control panel cards showing disk usage and incoming and outgoing bandwidth

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 KumaGatusUpptimecState
Runs onYour VPS, in DockerYour VPS, in DockerGitHub Actions and PagesAny static host
Configured inA web interfaceconfig.yaml.upptimerc.yml in a repositoryMarkdown files
ChecksHTTP, keyword, TCP, ping, DNS, push, game serversHTTP, TCP, UDP, ICMP, DNS, SSH, TLS, with conditionsHTTP, every 5 minutes at bestNone
IncidentsPosted on the status pageannouncements in the configGitHub issues, opened and closed for youFiles 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 sqlite block, 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 gatus after each edit.
  • Alerts fire after 3 failures in a row (failure-threshold) and resolve after 2 successes (success-threshold); send-on-resolved is off by default.
  • [CONNECTED] == true only 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 maintenance block 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"; and proxy_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 issuer with DNS 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 caddy tries 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 Host or X-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: