Skip to content

Smart home on K3s

Self-hosted home automation

Single-node Kubernetes cluster on a Raspberry Pi 5. Home Assistant, ArgoCD, Prometheus and Grafana, all GitOps-reconciled. Twenty-plus lights, plugs and sensors. Zero ports exposed to the internet.

2024 → ongoing
Single-node K3s, 20+ devices, 0 cloud accounts
phosphor P16 · violet
home/why-local

The same discipline, in miniature

Most smart-home setups end up as a pile of vendor apps tied together with cloud accounts. That works until a vendor goes away, or you realise your motion sensor is reporting to a server in another country.

I wanted the opposite: everything local, and every change to it in version control. Prometheus scrapes the lot. It's the same shape as the platform I run at work, sized to a flat.

One Raspberry Pi 5, K3s, ArgoCD, and a Zigbee dongle. The bill of materials fits on a postcard.

zigbee2mqtt/coordinator

One Pi, no cloud

A Raspberry Pi 5 (8GB) is the whole control plane. I run NVMe over USB because SD cards die under sustained writes, and a UPS on the power side because Home Assistant restarting at 3am after a tripped fuse isn't an experience I wanted twice. Hardwired ethernet, because Wi-Fi isn't a network.

A SONOFF Zigbee USB coordinator handles the radio. Devices pair directly with Zigbee2MQTT, which talks to Home Assistant over MQTT. No bridges, no cloud round-trip. Bulb to coordinator to broker to automation in single-digit milliseconds.

I'm at twenty-plus endpoints today: Hue bulbs, Innr plugs on power-monitored circuits, temperature and humidity sensors, motion and contact sensors, and a solar-powered camera. Every one of them reports to Prometheus.

// the path

Where a light switch press actually goes

The claim this whole build rests on. Same press, two topologies.

nothing leaves the building
wall switch
Zigbee press
coordinator
SONOFF USB dongle
Zigbee2MQTT
pod on the Pi
Home Assistant
automation fires
bulb
on

Every hop is on hardware in the flat. The internet connection can drop and the lights still work, because nothing on this path needed it. Remote access is a separate concern, handled by Tailscale rather than by opening a port.

argocd/apps · 6 synced

GitOps for the living room

Everything on the Pi is a Kubernetes deployment, reconciled by ArgoCD from a git repo. Adding a new automation, tweaking a Grafana dashboard, bumping the Home Assistant version. All of it goes through a commit. The cluster pulls; nothing pushes.

That sounds like overkill for a home lab, and it would be if it were any other tool. ArgoCD on K3s is genuinely 80MB of memory and a few CRDs. The payoff is a setup that survives me. If I blat the SD card tomorrow, a fresh install plus argocd app sync brings everything back.

argocd apps in the cluster
$ kubectl get applications -n argocd
NAME                  SYNC STATUS   HEALTH STATUS
home-assistant        Synced        Healthy
zigbee2mqtt           Synced        Healthy
mosquitto             Synced        Healthy
prometheus            Synced        Healthy
grafana               Synced        Healthy
node-exporter         Synced        Healthy
home/sensors/#

Power draw and humidity, in Grafana

Prometheus scrapes metrics from Home Assistant's exporter and from node-exporter on the Pi itself. Innr smart plugs report real-time power draw on the kitchen heater and the hallway lamp. SONOFF LCD sensors report temperature and humidity per room.

Grafana sits on top, with dashboards for power draw and room-by-room temperature. It's a small platform monitoring stack in every way that matters; the targets just happen to be bulbs and a kettle.

tailscale/status

Tailscale, not port-forwarding

Zero ports exposed to the internet. Remote access goes through Tailscale. Every device on my account joins a private overlay network and reaches the Pi by its tailnet IP. Nothing on the router needs opening.

The blast radius if Home Assistant is compromised is limited to the LAN, and the LAN is segmented so the IoT VLAN can't reach anything else. Boring threat-model, boring controls. That's the point.

home/decisions

Why it's built this way

Local-first by default

No vendor cloud, and no remote kill-switch someone else holds. If my internet goes down the lights still work. That one constraint picked most of the devices and the network topology for me.

Treat it like work

Same git, same ArgoCD, same Prometheus. The cost of applying real discipline to a home lab is small. The cost of not doing it is a fragile pile of config that nobody, me included, wants to debug at the weekend.

Right-sized infrastructure

K3s and ArgoCD aren't heavy. The whole control plane plus every workload runs comfortably on a single Pi 5 with room to spare. The temptation to add a second node for "HA" is a trap. For a flat, the right number of nodes is one.

home/backlog

Where this might go

Smart TRV radiator valves are next, which would let me schedule heating per room instead of per house. After that, presence detection that's good enough to stop relying on motion sensors. They're fine for "is someone in the hallway" and bad for "is anyone home".

The longer arc is local LLM integration: a small model on a separate node, so voice control and natural-language automation don't round-trip to a cloud API. Same instinct as the rest of the system. Local data, countable dependencies.

home/state

What it adds up to

Single-node

K3s + ArgoCD + Prometheus

20+

lights, plugs and sensors

6

GitOps-reconciled apps

0

ports exposed to the internet

What I actually get out of it: the house runs on kit I own and config I can read, and nothing in it phones home. When something breaks, I debug it the way I'd debug anything at work, and it stays fixed.

Thanks for reading.

If you also run ArgoCD at home, we'll get on.