You Don't Need Uptime Kuma for One Site

Raspberry Pi#ntfy#monitoring#systemd#self-hosted

Every self-hosting thread points at Uptime Kuma. For monitoring exactly one site with exactly one notification channel I actually check, a systemd timer and ntfy turned out to be the whole stack.

Every self-hosting subreddit thread about monitoring eventually points at Uptime Kuma, and it’s a genuinely good tool — a whole dashboard, dozens of notification integrations, a nice UI. But I already had exactly one thing I cared about monitoring (this site) and exactly one notification channel I actually check (ntfy, which I already push boot/shutdown alerts to from this same Pi). Standing up a whole extra Docker container and dashboard for one URL felt like the wrong amount of infrastructure for the problem.

So here’s the whole stack instead: a systemd timer, a shell script, and a topic on ntfy.sh.

The uptime check

#!/bin/bash
set -uo pipefail

URL="https://mynerdylife.com/"
STATE_FILE="/var/lib/site-uptime-check/state"
mkdir -p "$(dirname "$STATE_FILE")"

PREV_STATE="unknown"
[ -f "$STATE_FILE" ] && PREV_STATE="$(cat "$STATE_FILE")"

HTTP_CODE="$(curl -fsS -o /dev/null -w '%{http_code}' --max-time 15 "$URL" 2>/dev/null)"
CURR_STATE="up"; [ "$HTTP_CODE" = "200" ] || CURR_STATE="down"

if [ "$CURR_STATE" != "$PREV_STATE" ]; then
    if [ "$CURR_STATE" = "down" ]; then
        ntfy-notify.sh "mynerdylife.com is down" "Got HTTP ${HTTP_CODE:-no response}" "rotating_light,uptime" urgent
    elif [ "$PREV_STATE" = "down" ]; then
        ntfy-notify.sh "mynerdylife.com is back up" "Recovered — HTTP 200" "white_check_mark,uptime" default
    fi
fi

echo -n "$CURR_STATE" > "$STATE_FILE"

The state-file check is the only part that isn’t completely obvious, and it’s the part that matters: without it, a timer running every 5 minutes sends a “down” alert every 5 minutes for as long as the outage lasts, which is worse than useless — it trains you to ignore the topic. Comparing against the last known state means you get exactly two notifications per incident: one when it goes down, one when it recovers.

Wired to a systemd timer:

[Timer]
OnBootSec=2min
OnUnitActiveSec=5min
AccuracySec=30s

One deliberate choice: it checks the live public URL through Cloudflare, not localhost on the Pi itself. Checking locally would tell me nginx is fine, which is a different (and less useful) fact than “the internet can actually reach my site” — a Cloudflare-side outage or a DNS problem would look completely healthy from inside the house.

The other half: catching a silent monitoring pipeline

I’d separately built a Lighthouse CI pipeline that audits the site daily and stores performance/SEO/accessibility scores — and then, embarrassingly, forgotten it existed for a few days, because it ran and stored data but never told me anything. Same fix, same pattern: a small script parses the day’s scores, and if performance drops below 70 or accessibility/best-practices/SEO drop below 90, it fires the same ntfy-notify.sh helper. If the audit run itself fails outright — which on a Pi with no mail server configured would otherwise fail completely silently — that gets its own alert too.

The actual point

The pattern here isn’t “use ntfy specifically” — it’s that a notification channel you already trust and already check is worth more than a fancier dashboard you’ll forget to open. I have one topic, a handful of scripts that all shell out to the same ntfy-notify.sh helper, and zero new services to keep updated. For one site, that’s the right amount of monitoring.

(That same ntfy-notify.sh helper turned out to have more range than I expected — I wrote up what its query-string API can actually do after using it for something unrelated.)