System Architecture
Two compute layers, one event flow: device-bound hardware services on the Pis feed a Kubernetes cluster that stores, serves, and notifies.
Two layers
1. Hardware nodes (on the Pis, outside Kubernetes) are bound to physical devices — a camera and a Hailo AI accelerator — so they run as plain host services, not containers.
- Sensor node (Pi 4 + AI camera) streams MJPEG video on
:8080. The Pi 3s are not sensors — every one of them is a diskless k3s worker. - Inference node (Pi 5 + Hailo AI HAT+) consumes that stream, runs YOLO fire/person/smoke detection, and on a detection POSTs an event to the backend.
2. Cluster workloads (Kubernetes / k3s) — everything not tied to a physical device runs as a container:
- Backend — receives events, stores evidence images in MinIO and metadata in PostgreSQL, exposes a REST API, publishes to MQTT, and calls the Telegram bot on threats.
- MinIO — S3-compatible object store for evidence images (distributed, 4 nodes, erasure-coded).
- PostgreSQL (CloudNativePG) — replicated event-metadata database (1 primary + 2 standbys).
- Mosquitto — MQTT broker for live event push to the frontend.
- Frontend — React app (served by nginx), event list with evidence images, live over MQTT.
- Telegram bot — pure notifier; the backend calls it when something needs to be sent.
- Monitoring — Prometheus + Grafana, run separately via docker-compose (not a cluster workload).
End-to-end flow: a THREAT event to a notified user
| # | Step | Path |
|---|---|---|
| 1 | Sensor streams video | Pi 4/3 camera → MJPEG :8080 → Inference node |
| 2 | Inference detects & reports | YOLO (fire / person / smoke) → POST /api/events via reporter.py |
| 3 | Backend persists evidence | image → distributed MinIO · metadata row → PostgreSQL pg-rw |
| 4 | Threat notification | if type=THREAT: backend → POST /notify → Telegram bot → team |
| 5 | Live UI update | backend publishes events/new → Mosquitto → frontend (MQTT-over-WebSocket) |
The frontend also does a REST load of existing events on startup, and falls back to 5-second polling only when the MQTT socket is down — REST stays the source of truth; MQTT is a best-effort live-update layer on top of it.
Node roles
10.0.0.1edge-monitoringThe Pi 5 is far more than a control plane — it is simultaneously the NFS server holding every worker's root filesystem, the container image registry, the DHCP/TFTP boot server, the NAT gateway, the host of the inference node, and the only machine with a physical disk. That last fact matters a lot for storage durability — see Task 7 — Backend and Task 10 — Risk Register.
Communication protocols
| Link | Protocol |
|---|---|
| Sensor → inference | HTTP MJPEG stream (:8080) |
| Inference → backend | REST, POST /api/events |
| Frontend → backend | REST for initial load + fallback polling |
| Backend → frontend (events) | MQTT publish/subscribe via Mosquitto (live push) |
| Backend → Telegram bot | REST, fire-and-forget POST on THREAT |
| Backend → MinIO | S3 API (HTTP/REST) |
Local-to-Pi portability
Three choices make "build local on a Mac, deploy on the Pis later" work without manifest changes:
- Same distribution — local development used k3d, which runs k3s in Docker. The same distribution, not an identical cluster: the Pis additionally run containerd with
--snapshotter=native(overlayfs cannot use an NFS upper directory), plussystem-reservedandeviction-hardkubelet limits. Manifests still apply unchanged. - Same architecture — both the MacBook (Apple Silicon) and the Pis are arm64/aarch64, so images run natively, no cross-compilation.
- Same registry pattern — manifests always reference
edge-registry:5050/...; each node'sregistries.yamlmirrors that logical name to the real endpoint, so only the mirror config changes between environments, never the manifests.
Port map
| Component | Port | Notes |
|---|---|---|
| Sensor MJPEG | 8080 | host service, Pi 4 |
| Inference viewer | 8080 | host service, Pi 5, annotated MJPEG |
| Backend | 8000 | k8s service |
| Frontend | 80 | k8s service, via ingress |
| MinIO API / console | 9000 / 9001 | k8s service |
| Mosquitto (MQTT / WS) | 1883 / 9001 | WS exposed via ingress at /mqtt |
| Prometheus | 9090 | docker-compose, not the cluster |
| Grafana | 3000 | docker-compose, not the cluster |
We containerise movable software (backend, frontend) and keep device-bound software (camera, Hailo HAT) as host services, because a container scheduled onto a random worker can't reach a camera physically plugged into one specific Pi.