Skip to main content

Investigate: an external watchdog — does Uptime Kuma overlap the observability stack?

IMPLEMENTATION RULES: Before implementing this plan, read and follow:

Status: Backlog

Goal: Decide whether UIS needs an availability watchdog that runs outside the platform, and whether that duplicates the in-cluster observability stack.

Last Updated: 2026-08-07

Related: INVESTIGATE-system-observability — that covers depth inside the cluster; this covers survival outside it.


Questions to Answer

  1. Does an external watchdog duplicate Prometheus/Grafana/Loki?
  2. If not, where is the boundary between them?
  3. Where must it run, and what can it therefore not see?
  4. What is the failure mode of the watchdog itself?

Current State

The reference deployment runs the full stack on asgard — Prometheus, Alertmanager, Grafana, Loki, Tempo, otel-collector, node-exporter, kube-state-metrics. All healthy.

It has alerted on nothing, ever:

GET /api/v1/rules  ->  {"status":"success","data":{"groups":[]}}
PrometheusRule CRs -> 0
log agents (promtail/alloy/fluent/vector) -> 0

Findings

F1 — Every incident this week was found by a human asking a question

IncidentDuration before noticedHow it was found
iMac root filesystem went read-onlyhoursa write failed mid-task
M4 sleeping in 9–16 min cycles, repeatedlydayswent looking
Enrichment pipeline stopped8.4 hoursthe operator asked "is traffic flowing?"
1590 llava:7b failures (78% error rate)12 hsame query
Tailnet path to the database droppedminutesan SSH session hung
A CNPG replica orphaned when its primary died82 days, still reporting "Cluster in healthy state"found while removing it

Not one of these produced an alert. The last is the sharpest: a component reported itself healthy for 82 days while replicating from a host that no longer existed.

F2 — The overlap is real but shallow

Prometheus with blackbox_exporter can do HTTP/TCP/ICMP probes and alert on them. That is genuinely the same capability. So on paper, yes, it overlaps.

In practice the overlap is currently zero, because the in-cluster stack has no rules. But even fully configured, the duplicated part — "is this endpoint answering" — is the cheapest thing either system does. It is not where the cost or the value lies.

F3 — ⚠️ The decisive argument: a monitor inside the cluster cannot report the cluster being down

This is the same principle as production decision 4a backup job must not depend on the thing it backs up — applied to monitoring. The in-cluster stack is blind precisely when it matters most:

FailureIn-cluster stackExternal watchdog
One pod crashlooping✅ sees it✅ if it fronts a probed endpoint
Node under memory pressure✅ rich detail
k3s API downdies with it
The VM is off / host rebooteddies with it
Storage backend unreachable❌ likely dies with it
A batch job silently stopped❌ nothing emitsheartbeat expiry
The house lost power❌ (see F5)

The two systems answer different questions. In-cluster answers why; external answers whether. Neither substitutes for the other.

F4 — Heartbeat monitoring is the capability nothing else provides

The 8.4-hour pipeline stall (F1) is the important case, because nothing was broken. No pod crashed, no endpoint went down, no error rate spiked. Work simply stopped arriving. Metrics-based alerting struggles with absence; a push/heartbeat monitor — "tell me every N minutes that you are alive, and page me when you stop" — catches it directly.

The same applies to the backup chain. "Last successful backup older than N hours" is called out in INVESTIGATE-system-backup-and-scheduling as the highest-value alert a self-hosted platform can have, and it is a heartbeat, not a metric.

F5 — Where it runs determines what it can see, and there is no free lunch

LocationCatchesBlind to
In the monitored clusterpod/app failuresthe cluster, the node, the host
Another machine on the LANcluster/host/VM failure, batch stallspower cut, ISP outage
Outside the house (e.g. free-tier cloud)all of the aboveneeds inbound reach or an overlay

The reference deployment puts it on assist — a Raspberry Pi that is not part of the monitored platform. That covers every incident in F1. It does not cover a whole-house outage; that needs an off-site monitor and is a deliberate, separate decision.

F6 — "Who watches the watchdog" has no clean answer, only mitigations

If the watchdog dies, everything looks fine forever. Options, none free:

  • Accept it — the watchdog is small and simple; the platform is not.
  • Dead-man's switch — the watchdog pushes a heartbeat to a free external service; that service alerts if the push stops. Cheap, and closes the loop.
  • Mutual monitoring — the in-cluster stack probes the watchdog. This is where a little duplication is genuinely correct.

Recommend the dead-man's switch, with mutual monitoring once the in-cluster stack has any rules at all.

F7 — Storage on the reference host: measured, and counter-intuitive

assist boots from a 29 GB microSD and has a 57.7 GB USB stick already carrying Home Assistant, Z-Wave and k3s-storage, exposed as a local-path-usb StorageClass. The assumption was that the stick would be faster and safer:

                    boot microSD      USB DataTraveler 3.0
sequential write 33.3 MB/s 6.9 MB/s <- 5x SLOWER
4k sync writes 155/sec 136/sec <- slightly slower

It is not faster — a cheap flash stick on a 5 Gbps port, with no DRAM cache. It is also not obviously more durable. Its real advantages are blast radius (it is not the boot device; wearing it out costs a stick, not a rebuild) and capacity. Use it for that reason, not for speed.

At ~140 sync-writes/sec neither device wants a chatty SQLite workload, so check interval and retention must be set deliberately rather than left at defaults.

F8 — UIS has no concept of this

uis deploy targets a cluster. A watchdog that must live outside the cluster it watches is the same class of gap as the external database, object store, secret store, registry cache and scheduler — the recurring "components beside the cluster" class. It could be a UIS service deployed against a different kubeconfig, but nothing in UIS expresses "this must not run where the rest runs".


Options

Option A: Configure blackbox_exporter in the existing stack instead

Pros: one system, one skill set, alerts already route through Alertmanager. Cons: still inside the cluster, so blind to F3's bottom four rows. Solves the duplication worry by keeping the blind spot. Heartbeat monitoring needs Pushgateway plus custom rules — possible but fiddly.

Option B: Uptime Kuma on a machine outside the platform

Pros: correct blast radius; push/heartbeat monitors are first-class; trivial to run (~200 MB); a status page for free; notification channels built in. Cons: a second system to maintain; SQLite on flash needs tuning; overlaps ~10% with Prometheus.

Option C: Both, with a clear boundary

Pros: external answers whether, in-cluster answers why; each is used for what it is good at. Small duplication is deliberate redundancy at the layer where a single point of failure is unacceptable. Cons: two alerting paths to keep from becoming noisy.


Recommendation

Option C, implemented as Uptime Kuma on assist.

The overlap question resolves cleanly once the boundary is stated:

Uptime Kuma answers "is it up, and did the job run?" from outside. Prometheus/Grafana/Loki answer "why is it behaving like this?" from inside.

Concretely: do not rebuild in-cluster metrics as Kuma monitors, and do not rely on the in-cluster stack for availability of the cluster itself.

Two constraints that fall out of the findings:

  1. It must not run on asgard or on Odin (F3).
  2. Its own liveness needs a dead-man's switch (F6), or it is one silent failure away from being decorative — the exact failure the 82-day orphaned replica demonstrates.

Next Steps

  • PLAN-service-uptime-kuma-001-deploy.md — deploy on assist's k3s, USB-backed
  • PLAN-service-uptime-kuma-002-monitors.md — the monitor set, incl. heartbeats
  • PLAN-service-uptime-kuma-003-alerting.md — channels, escalation, dead-man's switch