Your Status Page Says Green and Your Users Say Otherwise
Someone messages you: “is the wiki down?” You open your status page. Everything is green. You open the wiki. It is down. The status page had no idea, because nothing was feeding it.
That last sentence is the whole difference between these three tools. Run Uptime Kuma if you want a point-and-click monitor with a status page attached. Run Gatus if you keep your infrastructure in Git and want health checks as YAML. Skip Cachet unless you specifically need an incident-communication portal and are willing to run a pre-release. Cachet does not monitor anything. It displays whatever you tell it.
Full example: Clone the working files at github.com/KingPin/sumguy-examples/observability/uptime-kuma-vs-gatus-vs-cachet
We already covered Uptime Kuma on its own in Uptime Kuma: Status Pages, Alerts, and Knowing Before Your Users Do. This post is the three-way comparison, so the focus is architecture and which one fits a home lab.
Three Tools, Three Different Jobs
The three tools look alike in screenshots. Each is a page of green dots. Underneath, they answer different questions.
- Uptime Kuma is a monitor with a GUI. You click to add checks, they live in a database, and a status page is one feature on top.
- Gatus is a monitor driven by a YAML file. The config is the source of truth, and the dashboard is built in.
- Cachet is a status page and incident tool. It stores components, incidents, and maintenance schedules. It has an API for something else to push status into.
Here is how they compare as of October 2026.
| Uptime Kuma | Gatus | Cachet | |
|---|---|---|---|
| Does its own checks | Yes | Yes | No |
| Config lives in | Database (GUI) | config.yaml | Database (admin UI) |
| Latest release | 2.5.6 (2026-10-09) | v5.37.0 (2026-09-24) | v2.4.1 (2023-11-07) |
| Storage | SQLite or MariaDB | memory, SQLite, or Postgres | SQLite, MySQL, MariaDB, Postgres |
| Runtime | Node.js | One Go binary | PHP 8.3 plus Composer |
| Official image | louislam/uptime-kuma | ghcr.io/twin/gatus | None for 3.x |
| Incident posts and updates | Yes, on status pages | Typed announcements with an archive, no update thread | Yes, the main feature |
| Push-based checks | Push monitors | External endpoints | API only |
| Best for | GUI lovers, mixed teams | GitOps, config-as-code | Public incident comms |
Versions come from each project’s GitHub releases API, checked on 2026-10-14. The Cachet row needs its own section.
Uptime Kuma: The One You Can Learn in Ten Minutes
Uptime Kuma is a Node.js app with a Vue front end. You add a monitor in the UI, pick a type, and it starts polling. The README lists HTTP(s), TCP, keyword, JSON query, WebSocket, ping, DNS record, push, Steam game server, and Docker container monitors. It lists 90+ notification services, including Telegram, Discord, Gotify, Slack, Pushover, and SMTP email.
The 2.x line went stable in October 2025 (2.0.0 shipped 2025-10-20). The headline change was MariaDB support alongside the default SQLite. For a home lab, SQLite is fine. MariaDB matters if you want a real database you can back up with the rest of your stack. The v1 to v2 jump had breaking changes and a migration guide, so read it before upgrading an old install. As of today the stable release is 2.5.6, and the master branch already carries a 3.0 beta version string. Pin to the 2 tag and skip the nightlies.
services: uptime-kuma: image: louislam/uptime-kuma:2 restart: unless-stopped ports: - "3001:3001" volumes: - uptime-kuma-data:/app/data
volumes: uptime-kuma-data:Open http://your-server:3001, create the admin account, and start adding monitors. Data lives in /app/data. Docker Hub also carries -slim and -rootless variants of each tag if you want a smaller or non-root image.
The Catch With Click-Ops
Your monitors live in a database, not in a file. You cannot code review a new check. You cannot rebuild the whole setup from a Git repo after a disk dies. You can back up the data volume, and you should. If your lab already has everything else in Compose files and Ansible, Kuma is the one piece you configure by hand.
For one person with a dozen services, that trade is usually fine. The GUI is faster than any YAML.
Gatus: Health Checks as Code
Gatus is a single Go binary. You hand it a config.yaml, and it checks every endpoint on a schedule, evaluates conditions you wrote, and serves a dashboard. The condition language is where it earns its keep. You are not just asking “is it up”. You are asserting what “up” means.
storage: type: sqlite path: /data/data.db
endpoints: - name: blog group: web url: "https://example.com/" interval: 60s conditions: - "[STATUS] == 200" - "[RESPONSE_TIME] < 300" - "[CERTIFICATE_EXPIRATION] > 48h"
- name: dns group: infra url: "1.1.1.1" dns: query-type: "A" query-name: "example.com" conditions: - "[DNS_RCODE] == NOERROR"That one file checks the status code, flags a slow response, and warns two days before a certificate expires. storage.type accepts memory, sqlite, or postgres. The default is memory, which forgets your uptime history on every restart. Set sqlite with a path on a volume, or your 30-day uptime graph resets to zero each time you redeploy.
The image is ghcr.io/twin/gatus, also published as twinproduction/gatus on Docker Hub. The README recommends the stable tag. I pin the version instead.
services: gatus: image: ghcr.io/twin/gatus:v5.37.0 restart: unless-stopped ports: - "8080:8080" environment: - CACHET_TOKEN=${CACHET_TOKEN:-unset} - PUSH_TOKEN=${PUSH_TOKEN:-change-me} volumes: - ./config.yaml:/config/config.yaml:ro - gatus-data:/data
volumes: gatus-data:Gatus expands ${VAR} references inside the config file, so secrets stay out of Git. Alerts are configured per provider under alerting, and an endpoint opts in with an alerts: list. Discord, Slack, Telegram, email, Gotify, and a long tail of others are supported.
Push Checks for Things Gatus Cannot Reach
Gatus also has external-endpoints. These are not polled. Something else pushes a result to Gatus with a bearer token. If nothing arrives within the heartbeat interval, Gatus records a failure. This is the equivalent of Kuma’s push monitors, and it is the right tool for cron jobs.
external-endpoints: - name: nightly-backup group: jobs token: "${PUSH_TOKEN}" heartbeat: interval: 26hThe push URL uses the key <group>_<name>, so this one is jobs_nightly-backup:
curl -fsS -X POST \ -H "Authorization: Bearer $PUSH_TOKEN" \ "http://localhost:8080/api/v1/endpoints/jobs_nightly-backup/external?success=true"Put that at the end of your backup script. Use success=false&error=... on failure. A 26 hour heartbeat on a daily job gives it two hours of slack before Gatus complains.
Where Gatus Is Thinner
The status page is a dashboard of results, not an incident log. Gatus has an announcements option: dated messages with a type, shown at the top of the page, and ones you mark archived move to a past-announcements list. If you want to post “we are investigating” and then “resolved” with a timeline, Gatus is not built for that. Also, you edit a file to add a check (Gatus reloads it on the fly). Fast for an engineer, annoying for a family member who just wants to add the Plex server.
Cachet: A Status Page Without a Monitor
Cachet is the odd one out. It has components (your services), incidents, maintenance schedules, subscribers, and metrics. It has no probe. It never calls your website. If your site dies, Cachet shows green until a human or a script changes the component status.
That is a legitimate design. Public-facing teams want a place to write a calm message while the real monitoring runs elsewhere. It is a poor fit if you hoped for a single box that watches your lab and shows a page.
The Honest State of the Project
I checked the repository directly, because Cachet’s reputation lags its reality.
- The latest tagged release is v2.4.1, from 2023-11-07. The 2.x line is effectively frozen.
- The
3.xbranch is a ground-up rewrite on Laravel and Filament. It requires PHP 8.3 or later, and the repo had commits as recently as 2026-10-05. - Cachet 3.x has no tagged release. The app’s
composer.jsonpins its core package atdev-main, and the version endpoint on a fresh install reports3.x-dev. - I found no maintained official Docker image. The
cachethq/dockerrepository was last pushed in June 2024, and its Docker Hub tags stop at 2.3.x from 2021.
So Cachet 3 is active, interesting, and pre-release. For a home lab that is acceptable if you enjoy tinkering. For anything people depend on, weigh it. Pinning to a moving branch means the next rebuild may not match the last one.
Running It Anyway
With no official image, you build one. This Dockerfile clones the 3.x branch and installs dependencies with Composer. I built and ran it for this post, with SQLite on a volume.
FROM php:8.3-cliRUN apt-get update && apt-get install -y --no-install-recommends git unzip libzip-dev libicu-dev libpng-dev libonig-dev \ && docker-php-ext-install pdo_mysql zip intl gd bcmath pcntl \ && rm -rf /var/lib/apt/lists/*COPY --from=composer:2 /usr/bin/composer /usr/bin/composerWORKDIR /appRUN git clone --depth 1 --branch 3.x https://github.com/cachethq/cachet.git . \ && cp .env.example .env \ && composer install --no-dev --optimize-autoloader --no-interactionCOPY entrypoint.sh /entrypoint.shEXPOSE 8000CMD ["sh", "/entrypoint.sh"]The entrypoint writes your settings into .env (the built-in dev server ignores most container variables), publishes assets, migrates, and serves. It runs php artisan serve, which is a development server. Put a reverse proxy in front and keep it on your LAN.
services: cachet: build: . restart: unless-stopped ports: - "8000:8000" env_file: .env volumes: - cachet-data:/data
volumes: cachet-data:cp .env.example .env# put a real key in APP_KEY: echo "base64:$(openssl rand -base64 32)"docker compose up -d --build
--password='pick-a-real-one' --admin=1 --name=adminYou also need an API token. I created one with php artisan tinker using Sanctum’s createToken() on the admin user. The entrypoint.sh and the full file set are in the example repo.
Pushing Status Into Cachet
Here is the part that makes Cachet useful. In 3.x, the API is under /api. Reads are public. Writes use a Sanctum bearer token. Component status is an integer:
| Value | Meaning |
|---|---|
| 1 | operational |
| 2 | performance issues |
| 3 | partial outage |
| 4 | major outage |
| 5 | unknown |
| 6 | under maintenance |
Create a component, then flip its status:
curl -X POST http://localhost:8000/api/components \ -H "Authorization: Bearer $CACHET_TOKEN" \ -H "Accept: application/json" -H "Content-Type: application/json" \ -d '{"name":"Blog","status":1}'
curl -X PUT http://localhost:8000/api/components/1 \ -H "Authorization: Bearer $CACHET_TOKEN" \ -H "Accept: application/json" -H "Content-Type: application/json" \ -d '{"status":4}'Both calls worked against my test instance. Note this is the 3.x API. The old 2.x API used X-Cachet-Token and different paths, so tutorials from 2020 will not match.
The Combo That Makes Sense: Gatus Feeding Cachet
If you want Cachet’s incident pages and real monitoring, let Gatus do the watching. Gatus has a custom alert provider that calls any URL, and a placeholders map that rewrites the triggered and resolved values. Map them to Cachet’s numbers:
alerting: custom: url: "http://cachet:8000/api/components/1" method: "PUT" headers: Authorization: "Bearer ${CACHET_TOKEN}" Content-Type: "application/json" Accept: "application/json" body: '{"status": [ALERT_TRIGGERED_OR_RESOLVED]}' placeholders: ALERT_TRIGGERED_OR_RESOLVED: TRIGGERED: "4" RESOLVED: "1"
endpoints: - name: blog group: web url: "https://example.com/" conditions: - "[STATUS] == 200" alerts: - type: custom failure-threshold: 3 success-threshold: 2 send-on-resolved: true description: "blog is down"I ran this with both containers on one Docker network and pointed Gatus at a dead URL. Gatus triggered the alert and the Cachet component flipped to “Major outage”. When the check recovers, the alert resolves and the component returns to operational. The custom alert URL is fixed per provider. One component per alert provider is simple. For many components, use overrides by group.
Is it worth the extra moving parts? For a home lab, usually no. You now run two services, a PHP app, and a bridge, to get a prettier incident log. Do it when strangers read your status page.
Which One Should You Run
- You want it working tonight, mostly through a browser: Uptime Kuma. Its monitor list covers HTTP, TCP, DNS, Docker, and push. Its status pages are good enough for a home lab and a few friends.
- Your lab is already in Git: Gatus. Checks become pull requests, and the condition syntax catches slow responses and expiring certificates that a plain up/down check misses.
- You need public incident write-ups and have a real audience: Cachet, fed by Gatus as shown above, understanding that 3.x is unreleased.
- You want one box and one answer: Uptime Kuma again. It is the lowest-effort path.
My own pick for a typical home lab is Uptime Kuma. Gatus comes second, and it wins the moment you care about keeping config in Git. Cachet only makes the list when “status page” means “communicate with other people” rather than “see what is broken”. A forklift is a fine machine, but you do not buy one to move a couch.
Whichever you pick, host it somewhere other than the machine it monitors. A status page that dies with your server tells nobody anything.
Common Questions
Does Cachet monitor my services automatically?
No. Cachet has no probes and never contacts your services. It stores component statuses and incidents that you or another tool set through its API or admin UI. Pair Cachet with Gatus, Uptime Kuma, or a script if you want the status to change when something breaks.
Is Cachet 3.x stable enough to use?
Cachet 3.x has no tagged release as of October 2026. The last tagged version is 2.4.1 from November 2023, and the 3.x branch reports itself as 3.x-dev. It is under active development and worked in my test, but treat it as pre-release software. Pin your build and keep backups.
Can Gatus send alerts and also show a public status page?
Yes. Gatus serves a built-in dashboard on port 8080 and sends alerts to providers such as Discord, Slack, Telegram, email, and Gotify. You can expose that dashboard publicly. It shows results and uptime history. It does not give you an incident timeline with written updates.
Should I back up Uptime Kuma’s data volume?
Yes. Uptime Kuma stores monitors, history, and status page settings in /app/data, either as SQLite or in a MariaDB database. Because nothing lives in a config file, losing that volume means re-adding every monitor by hand. Snapshot the volume on a schedule and test a restore once.