entry-31
Self-hosted alerting with Uptime Kuma and ntfy
I added real checks and tested alerts to an empty Kuma install, then put ntfy behind Caddy and learned why the credential a test sends matters more than the one I meant to paste.
revisions rev. 1 first set 2026-10-23 last revised 2026-10-23 3 min read
Update (2026-10-19): The domain migration and warning-route follow-up record later changes; the monitor inventory below is the September snapshot.
I already had Uptime Kuma 1.23.17 running on my netcup VPS behind Caddy. It had zero monitors and zero notification providers, so it was not checking the services I cared about or telling me when they failed.
Give it something to check
I added six monitors: HTTP checks with certificate-expiry notices for the blog, the portfolio, and the status host, a check on HTTPS port 443 at the edge, and DNS A lookups for stayz3ro.dev and chrisalorenzo.com against an external resolver. Each runs at a 60-second interval, with two retries 60 seconds apart. The resend setting is 30.
Discord was the first notification provider. I pointed a synthetic monitor at a URL I could break on purpose. After the retry threshold, DOWN arrived. I fixed the URL, saw UP arrive, and deleted the synthetic monitor. A notification test button only proves the webhook; the DOWN and UP messages proved the monitor, retries, and delivery path worked together.
Put ntfy behind the same edge
I added ntfy v2.28.0 on the same VPS, pinned by image digest. Caddy fronts it at ntfy.chrisalorenzo.com; the container publishes no port. I set access to deny by default and turned off signups and the web app. The root URL returns 404 on purpose.
The accounts have narrow jobs: one admin, one write-only user for each sender, and a read-only user for my phone. Kuma publishes to edge-alerts; home lab senders publish to lab-alerts. The topic boundary matters because a sender should not be able to publish anywhere else or read alerts back.
I checked the boundaries from outside. The root page and metrics returned 404. Anonymous publish and read both returned 403. Kuma’s user could publish to its topic and got 200, but got 403 when publishing to the wrong topic or trying to read. Those responses told me more than a successful publish alone would have.
Kuma reaches ntfy over plain HTTP on the internal Docker network. Trying HTTPS to the internal service gave connection refused on port 443. TLS belongs to Caddy at the public edge, not to that container-to-container hop.
The credential was the bug
The messy part was my own testing. A hidden read prompt in a pasted multi-line block consumed the next line instead of the token I thought I was entering. The request sent garbage and got 401. Twice, my clipboard held a password when I thought it held a token. Once, a full token ended up in a chat log; I revoked it and replaced it.
The final setup has Kuma sign in with its write-only user’s credentials, and I removed the unused tokens. The lesson for me: never chain a secret prompt inside a pasted block. Check a captured secret’s length and prefix without printing the value. It is the same lesson as the API key my coding agent was actually sending: verify what the tool sent, not what I meant to give it.
For instant push on my iPhone, the self-hosted server needs the upstream relay setting. With that arrangement, ntfy.sh sees message IDs only; the message content stays on my server.
What still needs an outside pair of eyes
Kuma watches ntfy’s health endpoint and sends that monitor’s alerts through Discord only. If ntfy breaks while the VPS stays up, the fallback channel can still tell me.
Both Kuma and ntfy still live on one VPS. If that VPS goes down, neither can alert me. The planned fix is a second monitoring vantage point elsewhere.
Related posts: Patching four cloud servers took more than apt upgrade, One firewall bug, two hosts, and a weekly report.