Skip to content
Go back

Uptime Kuma vs Gatus vs Cachet

By KingPin 13 min read
Uptime Kuma vs Gatus vs Cachet
Contents

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.

Here is how they compare as of October 2026.

Uptime KumaGatusCachet
Does its own checksYesYesNo
Config lives inDatabase (GUI)config.yamlDatabase (admin UI)
Latest release2.5.6 (2026-10-09)v5.37.0 (2026-09-24)v2.4.1 (2023-11-07)
StorageSQLite or MariaDBmemory, SQLite, or PostgresSQLite, MySQL, MariaDB, Postgres
RuntimeNode.jsOne Go binaryPHP 8.3 plus Composer
Official imagelouislam/uptime-kumaghcr.io/twin/gatusNone for 3.x
Incident posts and updatesYes, on status pagesTyped announcements with an archive, no update threadYes, the main feature
Push-based checksPush monitorsExternal endpointsAPI only
Best forGUI lovers, mixed teamsGitOps, config-as-codePublic 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.

docker-compose.yml
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.

config.yaml
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.

docker-compose.yml
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.

config.yaml
external-endpoints:
- name: nightly-backup
group: jobs
token: "${PUSH_TOKEN}"
heartbeat:
interval: 26h

The push URL uses the key <group>_<name>, so this one is jobs_nightly-backup:

Terminal window
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.

  1. The latest tagged release is v2.4.1, from 2023-11-07. The 2.x line is effectively frozen.
  2. The 3.x branch 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.
  3. Cachet 3.x has no tagged release. The app’s composer.json pins its core package at dev-main, and the version endpoint on a fresh install reports 3.x-dev.
  4. I found no maintained official Docker image. The cachethq/docker repository 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.

Dockerfile
FROM php:8.3-cli
RUN 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/composer
WORKDIR /app
RUN git clone --depth 1 --branch 3.x https://github.com/cachethq/cachet.git . \
&& cp .env.example .env \
&& composer install --no-dev --optimize-autoloader --no-interaction
COPY entrypoint.sh /entrypoint.sh
EXPOSE 8000
CMD ["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.

docker-compose.yml
services:
cachet:
build: .
restart: unless-stopped
ports:
- "8000:8000"
env_file: .env
volumes:
- cachet-data:/data
volumes:
cachet-data:
Terminal window
cp .env.example .env
# put a real key in APP_KEY: echo "base64:$(openssl rand -base64 32)"
docker compose up -d --build
docker compose exec cachet php artisan cachet:make:user [email protected] \
--password='pick-a-real-one' --admin=1 --name=admin

You 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:

ValueMeaning
1operational
2performance issues
3partial outage
4major outage
5unknown
6under maintenance

Create a component, then flip its status:

Terminal window
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:

config.yaml
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

  1. 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.
  2. 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.
  3. You need public incident write-ups and have a real audience: Cachet, fed by Gatus as shown above, understanding that 3.x is unreleased.
  4. 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.


Share this post on:

Send a Webmention

Written about this post on your own site? Send a webmention and it'll show up above once verified.


Previous Post
Agent Memory Without a Vector DB
Next Post
What Actually Fills Your Loki Storage

Discussion

Powered by Garrul . Sign in with GitHub or Google, or post anonymously.

Related Posts