ops-readonly gets kuma_alerts (host export kuma-export.py, one more read-only mount). kuma-triage.py sorts the same data without a model. Flow H runs the agent with skill alert-triage.md. Flow B: one open incident per monitor, an UP closes all of them, titles name start and end and stay under Outline's limit. B and D accept Kuma only with a header token. Monitoring notes: ntfy rejects Kuma messages for monitors without a URL, resend for long outages.
178 lines
7.8 KiB
Markdown
178 lines
7.8 KiB
Markdown
# Monitoring and alerting - notice before your customer does (episode 6)
|
|
|
|
Uptime Kuma has been in the stack since episode 1, but a monitor nobody hears
|
|
is a dashboard, not an alarm. This episode adds three things: a push channel
|
|
you own (ntfy), a public status page on its own hostname, and a second Kuma
|
|
outside the server, because a box cannot report its own death.
|
|
|
|
## ntfy: the push channel
|
|
|
|
One container, one topic. Kuma publishes to `alerts`, your phone subscribes to
|
|
it with the ntfy app (Android, iOS, or the web UI at `https://ntfy.<DOMAIN>`).
|
|
|
|
```yaml
|
|
ntfy:
|
|
image: binwiederhier/ntfy:v2.28.0
|
|
restart: unless-stopped
|
|
command: serve
|
|
environment:
|
|
NTFY_BASE_URL: https://ntfy.${DOMAIN}
|
|
NTFY_BEHIND_PROXY: "true"
|
|
NTFY_AUTH_DEFAULT_ACCESS: deny-all # nobody reads or writes without a user
|
|
NTFY_AUTH_FILE: /var/lib/ntfy/user.db
|
|
NTFY_CACHE_FILE: /var/cache/ntfy/cache.db
|
|
NTFY_ATTACHMENT_CACHE_DIR: /var/cache/ntfy/attachments
|
|
NTFY_ENABLE_LOGIN: "true"
|
|
volumes:
|
|
- ntfy_data:/var/lib/ntfy
|
|
- ntfy_cache:/var/cache/ntfy
|
|
networks: [proxy]
|
|
```
|
|
|
|
Caddy block (websockets and long polling need nothing special):
|
|
|
|
```
|
|
ntfy.{$DOMAIN} {
|
|
reverse_proxy ntfy:80
|
|
}
|
|
```
|
|
|
|
Start it, reload Caddy, then create two users: one that may only write to the
|
|
topic (that is Kuma) and one that may read it (that is you).
|
|
|
|
```bash
|
|
docker compose up -d ntfy
|
|
docker compose exec caddy caddy reload --config /etc/caddy/Caddyfile
|
|
|
|
docker compose exec -e NTFY_PASSWORD='<kuma password>' ntfy ntfy user add --role=user kuma
|
|
docker compose exec -e NTFY_PASSWORD='<your password>' ntfy ntfy user add --role=user max
|
|
docker compose exec ntfy ntfy access kuma alerts write-only
|
|
docker compose exec ntfy ntfy access max alerts read-write
|
|
docker compose exec ntfy ntfy access # prints the whole table
|
|
```
|
|
|
|
Prove the lock works before you trust it:
|
|
|
|
```bash
|
|
# anonymous publish: 403
|
|
curl -d "anyone can post here?" https://ntfy.<DOMAIN>/alerts
|
|
# authenticated publish: lands
|
|
curl -u kuma:<pw> -H "Title: Hello from the box" -d "First message" https://ntfy.<DOMAIN>/alerts
|
|
# read it back as a reader
|
|
curl -u max:<pw> "https://ntfy.<DOMAIN>/alerts/json?poll=1"
|
|
```
|
|
|
|
## Uptime Kuma: notification, monitors, status page
|
|
|
|
All of this is done in the Kuma web UI at `https://status.<DOMAIN>/dashboard`.
|
|
|
|
1. Settings -> Notifications -> Setup Notification. Type **ntfy**, server
|
|
`https://ntfy.<DOMAIN>`, topic `alerts`, authentication *username and
|
|
password* with the `kuma` user. Tick *Default enabled* and *Apply on all
|
|
existing monitors*, press *Test*, watch it arrive on your phone.
|
|
2. Monitors. Public services get an **HTTP(s) - Keyword** monitor, because a
|
|
200 from a reverse proxy says nothing about the app behind it. Keywords
|
|
that work with the stack: `Forgejo`, `Outline`, `Vaultwarden`, `NetBird`.
|
|
Interval 60 s, retries 1, retry interval 20 s, *Certificate Expiry
|
|
Notification* on. Settings -> Notifications -> TLS expiry: 7, 14, 21 days.
|
|
3. Services that left the public internet in episode 5 are monitored from
|
|
inside the Docker network. n8n: `http://n8n:5678/healthz`. Kuma sits on
|
|
the same `proxy` network, so the container name resolves.
|
|
4. Groups `Public` and `Internal` keep the dashboard readable. A **Push**
|
|
monitor `Nightly backup` with a 90000 s interval turns a backup job into
|
|
something that alerts when it *does not* run: the job calls the push URL,
|
|
Kuma complains when the call is missing for 25 hours.
|
|
5. Status page: slug `status`, title *Company Status*, add only the public
|
|
monitors. Under *Domain Names* enter `status.<DOMAIN>`: Kuma then serves
|
|
the page at the root of that hostname and keeps the dashboard at
|
|
`/dashboard`. No extra Caddy rule needed.
|
|
|
|
Verify:
|
|
|
|
```bash
|
|
curl -s https://status.<DOMAIN>/ | grep -o "<title>[^<]*</title>" # Company Status
|
|
curl -s https://status.<DOMAIN>/api/status-page/status | python3 -m json.tool
|
|
```
|
|
|
|
## The second eye
|
|
|
|
Stop the server and both Kuma and ntfy stop with it: no alert. So a second
|
|
Uptime Kuma runs on a machine that is not that server (a laptop at home, a
|
|
Raspberry Pi, a colleague's NAS). LAN only, no public route.
|
|
|
|
```yaml
|
|
name: kuma-home
|
|
services:
|
|
uptime-kuma:
|
|
image: louislam/uptime-kuma:1
|
|
restart: unless-stopped
|
|
ports:
|
|
- "3001:3001"
|
|
volumes:
|
|
- kuma_data:/app/data
|
|
volumes:
|
|
kuma_data:
|
|
```
|
|
|
|
Its monitors: keyword on `https://status.<DOMAIN>/` (keyword *Company Status*),
|
|
keyword on `https://git.<DOMAIN>/` (keyword *Forgejo*), ping on the server IP.
|
|
Its notification must not depend on the server either, so it uses the public
|
|
`https://ntfy.sh` with a random, unguessable topic (`sho-` plus twelve random
|
|
characters). Subscribe to that topic in the same ntfy app, next to your own.
|
|
|
|
## Rehearse the outage
|
|
|
|
Do not wait for the real one. `docker stop selfhostedops-stack-outline-1`,
|
|
start a stopwatch, `docker start` it when the phone buzzes. With the settings
|
|
above the DOWN alert takes between 20 and 80 seconds (interval plus one
|
|
retry), the UP alert follows about a minute after the container is healthy.
|
|
|
|
## Gotchas
|
|
|
|
- `NTFY_AUTH_DEFAULT_ACCESS: deny-all` also blocks the *reader*. A user with
|
|
`write-only` cannot poll the topic (403). Give yourself `read-write`, give
|
|
Kuma `write-only`, never the other way round.
|
|
- The ntfy CLI takes the password from the `NTFY_PASSWORD` environment
|
|
variable. Pass it with `docker compose exec -e NTFY_PASSWORD=...`;
|
|
`ntfy user add` without it wants a terminal and hangs in scripts.
|
|
- A plain HTTP monitor is happy with any 2xx. Caddy answers 2xx for a dead
|
|
backend in some setups (a login page, a maintenance page). Use keyword
|
|
monitors for anything a customer sees.
|
|
- Switching a monitor from a dead public URL to the internal one fires an
|
|
*Up* notification the moment it works again. Expected, but it surprises
|
|
people the first time.
|
|
- Kuma 1.x refuses monitor intervals above 24 days and the push monitor's
|
|
interval is also its grace period: 90000 s means "alert after 25 hours
|
|
without a ping", not "check every 25 hours".
|
|
- The status page is rendered server side for the domain root, the
|
|
dashboard stays a single page app. A keyword monitor on the status page
|
|
therefore works (the title is in the HTML), a keyword monitor on
|
|
`/dashboard` does not.
|
|
- The *Domain Names* field only works when the request really arrives with
|
|
that `Host` header. Behind Caddy that is the default; behind a proxy that
|
|
rewrites the host header, set `trustProxy` in Kuma's settings and forward
|
|
the header.
|
|
- The second Kuma uses the public ntfy.sh without authentication. Anyone who
|
|
guesses the topic can read your alerts and post fake ones. Twelve random
|
|
characters make guessing impractical; still, do not put secrets in monitor
|
|
names.
|
|
- `louislam/uptime-kuma:1` is the 1.x line (1.23.17 at recording time).
|
|
2.x exists (2.5.3) and migrates the database on first start. Pin what you
|
|
test, upgrade on purpose, not by `docker compose pull`.
|
|
|
|
|
|
## What episode 16 found in this setup
|
|
|
|
- **ntfy rejects Kuma's message for monitors without a URL.** Ping, port,
|
|
group and push monitors have no URL; Kuma still adds a "view" button and
|
|
ntfy answers `400 parameter 'url' is required for action 'view'`. The alert
|
|
never arrives, only the Kuma log shows it. Give those monitors a URL (any
|
|
page, for example the status page); in the Kuma database that is the `url`
|
|
column, the edit form hides it for these types.
|
|
- **Kuma only alerts on a change.** A monitor that stays down is announced
|
|
once. Set "Resend Notification if Down X times consecutively" (for a 60 s
|
|
interval, 60 means once an hour), otherwise a twelve day outage looks
|
|
exactly like a one minute one.
|
|
- **Every Kuma restart announces every down monitor again.** After a restart
|
|
the first beat is PENDING, the next DOWN counts as a change. Anything that
|
|
opens tickets from Kuma must skip a DOWN for a monitor that is already open.
|