Self-hosted real-time monitoring for a k3s cluster: FastAPI backend + React frontend in a single container, live updates over WebSocket, highly available with 2 replicas (a node goes down ⇒ the second replica takes over, the frontend reconnects automatically).
Live demo — running the actual
ghcr.io/lukislp/piwatch:latest image published by this repo's own CI/CD pipeline, in its
built-in demo mode (PIWATCH_DEMO=1): a simulated 3-node Raspberry Pi cluster with
live-changing CPU/memory/temperature/disk metrics, pods, deployments, and events - no real
cluster involved.
- Kubernetes live: nodes, pods, deployments, events via the watch API (no polling)
- Pi hardware: CPU temperature, load, RAM, disk, uptime per node (DaemonSet agent)
- Metrics: CPU/RAM usage via metrics-server (bundled with k3s), ~3h history
- HTTP/TCP healthchecks for your own services (Home Assistant, MQTT, …) with uptime history
- Live logs for any pod, right in the browser
- Simple auth: password from a Kubernetes Secret, signed tokens (failover-friendly)
- Dark/light mode
Browser ⇄ WSS ⇄ Gateway/Ingress ⇄ Service ⇄ 2× piwatch pod (anti-affinity)
│ FastAPI + React static files
│ Watcher/poller/checks (independent per replica)
DaemonSet node-agent (1/Pi, /sys + /proc read-only)
Each replica keeps its own state in RAM (stateless externally, no shared storage, no leader election). Tokens from both replicas are interchangeable because both use the same Secret.
cd backend && pip install -r requirements.txt
PIWATCH_DEMO=1 uvicorn app.main:app --port 8000
# Frontend dev server (optional, with proxy):
cd ../frontend && npm install && npm run devWithout PIWATCH_PASSWORD, login is disabled.
The manifests in deploy/ are written for a 2-node k3s cluster and route
through the Gateway API (httproute.yaml,
tested with NGINX Gateway Fabric). If you're on ingress-nginx or another
controller, swap httproute.yaml/RateLimitPolicy for an Ingress resource
and adjust deploy/kustomization.yaml accordingly.
Before deploying, adjust the hostnames in deploy/httproute.yaml to point at
your own domains.
-
Image: the manifests in
deploy/referenceregistry.example.com/your-namespace/piwatch:latestas a placeholder — for a stock, unmodified deployment, just point them at the multi-arch image this repo's own CI/CD pipeline already builds and publishes on every release,ghcr.io/lukislp/piwatch:latest(or a pinned:X.Y.Zversion tag). You only need to build your own image if you've forked or modified the code:docker buildx build --platform linux/arm64,linux/amd64 \ -t your-registry/your-namespace/piwatch:latest --push .Either way, running pods need a restart to pull a newly-pushed
:latest(imagePullPolicy: Alwayspicks it up automatically, but won't force a restart on its own):kubectl -n monitoring rollout restart deployment/piwatch daemonset/piwatch-node-agent. -
Create the Secret —
.\create-secret.ps1(interactively asks only for the login password, generates the signing key automatically at random, creates the Secret directly on the cluster viakubectl— it never touches disk as a plaintext file). The alternative, file-based approach (cp deploy/secret.example.yaml deploy/secret.yaml+ uncomment the- secret.yamlline indeploy/kustomization.yaml) still works but is not recommended, since the Secret then sits unencrypted on disk. For a Secret that's safe to commit to Git, seal it with Sealed Secrets instead (see the comment indeploy/kustomization.yaml). -
Roll it out (point
KUBECONFIGat your cluster's kubeconfig):kubectl apply -k deploy/ kubectl -n monitoring get pods -o wide # 2× piwatch across 2 nodes + node-agents -
Access: whichever hostnames you configured in
deploy/httproute.yaml(e.g. an internal.lanname plus a public domain behind your reverse proxy).
kubectl drain <node-running-a-piwatch-pod> --ignore-daemonsets --delete-emptydir-dataThe dashboard stays reachable: the browser reconnects automatically and gets
the full state from the surviving replica. Afterwards:
kubectl uncordon <node>.
cd backend && python -m pytest tests/| Variable | Meaning | Default |
|---|---|---|
PIWATCH_PASSWORD |
Login password (empty = auth disabled) | – |
PIWATCH_SECRET |
Token signing key (must match across replicas) | derived |
PIWATCH_TOKEN_TTL |
Token validity in seconds | 43200 |
PIWATCH_DEMO |
1 = demo mode with fake data |
– |
PIWATCH_CHECKS_FILE |
Path to the healthcheck YAML | /config/healthchecks.yaml |
PIWATCH_AGENT_SERVICE |
Headless service for the node-agents | piwatch-node-agent.monitoring… |
- History data lives only in RAM (~3h); it resets on pod restart. Possible extension: SQLite/PVC or Prometheus.
- No alerting (push/mail) — the pub/sub structure is prepared for it.
