Overview

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

#StepPath
1Sensor streams videoPi 4/3 camera → MJPEG :8080 → Inference node
2Inference detects & reportsYOLO (fire / person / smoke) → POST /api/events via reporter.py
3Backend persists evidenceimage → distributed MinIO · metadata row → PostgreSQL pg-rw
4Threat notificationif type=THREAT: backend → POST /notify → Telegram bot → team
5Live UI updatebackend 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

Control plane
1× Raspberry Pi 5k3s server, 10.0.0.1
Workers
8× Raspberry Pi 3diskless k3s agents
Cluster
k3s v1.36.2Debian 13 (trixie), arm64
Namespace
edge-monitoring

The 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

LinkProtocol
Sensor → inferenceHTTP MJPEG stream (:8080)
Inference → backendREST, POST /api/events
Frontend → backendREST for initial load + fallback polling
Backend → frontend (events)MQTT publish/subscribe via Mosquitto (live push)
Backend → Telegram botREST, fire-and-forget POST on THREAT
Backend → MinIOS3 API (HTTP/REST)

Local-to-Pi portability

Three choices make "build local on a Mac, deploy on the Pis later" work without manifest changes:

  1. 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), plus system-reserved and eviction-hard kubelet limits. Manifests still apply unchanged.
  2. Same architecture — both the MacBook (Apple Silicon) and the Pis are arm64/aarch64, so images run natively, no cross-compilation.
  3. Same registry pattern — manifests always reference edge-registry:5050/...; each node's registries.yaml mirrors that logical name to the real endpoint, so only the mirror config changes between environments, never the manifests.

Port map

ComponentPortNotes
Sensor MJPEG8080host service, Pi 4
Inference viewer8080host service, Pi 5, annotated MJPEG
Backend8000k8s service
Frontend80k8s service, via ingress
MinIO API / console9000 / 9001k8s service
Mosquitto (MQTT / WS)1883 / 9001WS exposed via ingress at /mqtt
Prometheus9090docker-compose, not the cluster
Grafana3000docker-compose, not the cluster
Container vs. host service, in one line

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.