Gatus status page
Uptime checks for every service, written as YAML, with a status page and ntfy alerts.
- Standalone container
- Rootless: systemctl --user
- Tested with podman 5.0 and newer
Last updated
See where this fits on the homelab connections map, with every port it uses.
In short: Gatus reads its checks from one config.yaml that you keep in ~/.config/gatus and mount read-only, so the checks are versioned like code. It runs rootless with Network=host, because only then can it test the real path through DNS, Caddy and TLS to the server's own LAN address. The ntfy token comes from a podman secret and alerts fire after 5 failed checks in a row.
The quadlet files
Every quadlet file below passes podman's quadlet generator and
systemd-analyze verify on podman 5.0 and the current release. Personal values
are placeholders: replace example.com, /srv/... paths and the
time zone with your own.
gatus.container
Save as ~/.config/containers/systemd/gatus.container
[Unit]
Description=Gatus uptime checks and status page
StartLimitIntervalSec=0
After=network-online.target ntfy.service
[Container]
Image=docker.io/twinproduction/gatus:latest
ContainerName=gatus
AutoUpdate=registry
# Host network: from a rootless container the host's own LAN address is not reachable,
# so the real path (DNS -> Caddy -> TLS -> app) could not be tested otherwise.
# Gatus listens on port 8084 (web.port in config.yaml).
Network=host
Environment=TZ=Etc/UTC
Environment=GATUS_CONFIG_PATH=/config/config.yaml
# ntfy token for alerts, as env NTFY_ALERT_TOKEN (config.yaml reads ${NTFY_ALERT_TOKEN}).
Secret=ntfy-alert-token,type=env,target=NTFY_ALERT_TOKEN
Volume=%h/.config/gatus:/config:ro,Z
Volume=gatus.volume:/data:Z
# No HealthCmd: the image is built FROM scratch, without a shell, curl or wget.
[Service]
Restart=always
RestartSec=30
[Install]
WantedBy=default.target
gatus.volume
Save as ~/.config/containers/systemd/gatus.volume
[Unit]
Description=Gatus data: SQLite history of every check
[Volume]
VolumeName=gatus
config.yaml
Save as ~/.config/gatus/config.yaml
# Gatus config as code: ~/.config/gatus/config.yaml, mounted read-only.
# Replace example.com and 10.0.0.x with your own names and addresses.
storage:
type: sqlite
path: /data/data.db
web:
port: 8084
ui:
title: Home server status
header: Home server
alerting:
ntfy:
url: "http://127.0.0.1:2586"
topic: "homelab-alerts"
token: "${NTFY_ALERT_TOKEN}"
priority: 4
default-alert:
# 5 failed checks in a row (10 minutes at a 2m interval) before an alert.
failure-threshold: 5
success-threshold: 2
send-on-resolved: true
endpoints:
# The full path a browser takes: DNS, Caddy, a valid certificate, the app.
- name: Jellyfin
group: web (HTTPS via Caddy)
url: "https://jellyfin.example.com/health"
interval: 2m
client:
timeout: 15s
conditions:
- "[STATUS] < 500"
- "[CERTIFICATE_EXPIRATION] > 240h"
alerts:
- type: ntfy
- name: Radarr
group: web (HTTPS via Caddy)
url: "https://radarr.example.com/ping"
interval: 2m
client:
timeout: 15s
conditions:
- "[STATUS] < 500"
alerts:
- type: ntfy
# The app itself, without Caddy: tells a dead app apart from a proxy problem.
- name: Jellyfin (backend)
group: backends (direct)
url: "http://127.0.0.1:8096/health"
interval: 2m
conditions:
- "[STATUS] == 200"
alerts:
- type: ntfy
- name: Local DNS answers
group: infrastructure
url: "10.0.0.10"
interval: 2m
dns:
query-name: "jellyfin.example.com"
query-type: "A"
conditions:
- "[DNS_RCODE] == NOERROR"
- "[BODY] == 10.0.0.10"
alerts:
- type: ntfy
- name: NAS (NFS port)
group: infrastructure
url: "tcp://10.0.0.11:2049"
interval: 2m
conditions:
- "[CONNECTED] == true"
alerts:
- type: ntfy
# No alert here: if ntfy is down, it cannot deliver the alert anyway.
- name: ntfy
group: infrastructure
url: "http://127.0.0.1:2586/v1/health"
interval: 2m
conditions:
- "[STATUS] == 200"
- "[BODY].healthy == true"
Caddyfile
Save as /etc/caddy/Caddyfile
# Gatus has no login: keep the status page on your LAN or VPN, or add basic_auth.
status.example.com {
reverse_proxy 10.0.0.10:8084
}
Why is it built this way?
What it is. Gatus checks your services on a
schedule and shows the result on a status page. Every check is a few lines of YAML with conditions
such as [STATUS] == 200, so adding a service is a small edit in one file, not
clicks in a web UI.
Config as code. config.yaml lives in
~/.config/gatus/ and is mounted read-only. Keep it in git next to your quadlets. Only
the check history goes into gatus.volume; losing it costs you graphs, not settings.
Why host networking. A rootless container reaches the internet and other published
ports, but not the host's own LAN address. To test what a browser really does (ask DNS, connect to
Caddy, check the certificate), Gatus has to sit on the host's network. The same rule explains why
some checks use 127.0.0.1: with Network=host, that is the server itself.
The token stays out of the YAML. Gatus replaces ${NTFY_ALERT_TOKEN}
with the environment variable, and the quadlet fills that variable from the podman secret
ntfy-alert-token. The config file can be shared or committed without leaking anything.
The ntfy example shows how to create a
write-only account and its token.
Caddy in front of Gatus
Gatus has no login. Put the status page on a name that only resolves on your LAN or VPN, or add
basic_auth to the site block. The
Caddyfile generator writes the block for you.
Check that it works
systemctl --user status gatus.service
# JSON with the latest result of every check
curl -s http://127.0.0.1:8084/api/v1/endpoints/statuses | head -c 400; echo
journalctl --user -u gatus.service -n 30 Want an alert when Gatus itself stops? The OnFailure= hook from the ntfy example covers it. For the basics of user services, see quadlet basics.
How do I start it?
Run these as your normal user, not as root. Quadlet reads ~/.config/containers/systemd/ for user services.
mkdir -p ~/.config/containers/systemd ~/.config/gatus
# the ntfy token of a write-only app account (see the ntfy example)
podman secret create ntfy-alert-token ~/.config/ntfy/alert-token
# save gatus.container, gatus.volume and config.yaml, then:
systemctl --user daemon-reload
systemctl --user start gatus.service
# open http://<server>:8084
Check the result with systemctl --user status and
the logs with journalctl --user -u followed by the
service name. To turn a docker-compose file of your own into files like these, use the
compose to quadlet converter.
Common mistakes
Gatus times out on the server's own LAN address or its HTTPS names
A rootless container with the default pasta network cannot reach the host's own LAN IP. Network=host solves it; Gatus then listens directly on the host's port 8084, so there is no PublishPort= line.
Gatus exits at start with an error about the config file
Gatus refuses unknown keys and bad conditions. Read the exact line with journalctl --user -u gatus.service -n 30. A config change needs systemctl --user restart gatus.service unless you enable Gatus's own config reload.
Alerts arrive for every short hiccup
failure-threshold counts failed checks in a row. With interval 2m and failure-threshold 5, an app must be down for about 10 minutes before you hear about it. send-on-resolved: true tells you when it is back.
The ntfy alert fails with 401 or 403
Check that the secret exists (podman secret ls) and that the account may write to the topic. After recreating a secret, restart gatus.service: secrets are read when the container starts.
Frequently asked questions
Why no health check in the Gatus quadlet?
The Gatus image is built FROM scratch and contains only the Gatus binary, without a shell, curl or wget, so there is nothing to run a HealthCmd= with. systemd still restarts it when the process dies, and an external check can watch its web port.
Should I check the app directly or through Caddy?
Both. The HTTPS check proves the whole path works: DNS, Caddy and the certificate. The direct check against 127.0.0.1 tells you whether the app itself is down or only the proxy path is broken.
Can Gatus check DNS and a NAS, not only websites?
Yes. A dns: block queries a DNS server and can compare the answer, and a tcp:// URL checks that a port accepts connections, such as NFS on port 2049 of a NAS.