Home Assistant on Kubernetes: A Questionable Decision I'd Make Again
June 2026 · 8 min read
Home Assistant's documentation gently steers you toward running it on a dedicated box. A Raspberry Pi, their own appliance OS, maybe a container if you insist. What it does not suggest is running it as a workload on a Kubernetes cluster, because that is a strange thing to want.
I run mine on the k3s cluster in my living room anyway, deployed by Argo CD from a Git repo like everything else in the house. This post is the honest accounting. Which Kubernetes concepts survive contact with a smart home hub, which ones bend, and where the workarounds live.
A stateful singleton in a world built for replicas
Start with what Home Assistant is, architecturally: a singleton with a SQLite database and strong feelings about being the only one of itself. Two instances writing one config directory is corruption with extra steps. So the deployment renounces most of what Kubernetes is famous for:
spec:
replicas: 1
strategy:
type: Recreate
Recreate instead of the default rolling update matters more than it looks. A rolling update briefly runs old and new pods side by side. For a stateless API that's zero-downtime deployment. For Home Assistant it's two processes fighting over one SQLite file. Recreate kills the old pod fully before starting the new one. I traded thirty seconds of "the lights are thinking" during upgrades for not corrupting my smart home's memory.
State lives on two PersistentVolumeClaims. 10 GiB for the config volume, which looks generous until you remember the recorder database grows with every sensor reading your house ever emits. And a separate 1 GiB volume for the Matter server, kept apart so the two processes never share a write path. Both are ReadWriteOnce, which on a single-node cluster is less a restriction than a statement of fact. Backups ride the same GitOps philosophy as everything else. The manifests in Git rebuild the shape of the deployment in minutes. The PVC contents are what actually needs backing up, and they're small enough that snapshotting them is a cron job, not a project.
Everything about this pod, drawn in one picture:
flowchart TB
subgraph NODE["NUC (master node — pod pinned here)"]
subgraph POD["Home Assistant pod (hostNetwork: true)"]
INIT["init: busybox<br/>seed configuration.yaml<br/>append http: proxy block"] --> HA["home-assistant:stable"]
INIT --> MS["matter-server:stable<br/>(sidecar)"]
end
CFG[("PVC 10Gi<br/>/config")] --- HA
MDATA[("PVC 1Gi<br/>matter data")] --- MS
CM["ConfigMap<br/>config template"] -.seed only.-> INIT
end
LAN["smart devices<br/>mDNS · SSDP · Matter"] <-->|multicast heard via host NIC| POD
WEB["browser"] --> TR["Traefik ingress"] --> HA
The hostNetwork confession
The least Kubernetes-native line in the manifest, and the most necessary:
hostNetwork: true
dnsPolicy: ClusterFirstWithHostNet
Smart home devices announce themselves with multicast. mDNS, SSDP. Cast devices, ESPHome nodes, half the discoverable world shouts into the local network and expects the hub to hear it. A pod on the cluster's overlay network is deaf to all of it, because the multicast never crosses the bridge. hostNetwork: true puts Home Assistant directly on the NUC's network interface, where it can hear the house.
The subtle tax is that second line. With host networking, a pod defaults to the host's DNS, and suddenly it can't resolve cluster-internal service names. ClusterFirstWithHostNet restores cluster DNS while keeping host networking. I now check that combination by reflex on any hostNetwork pod, having learned it the way this is always learned: forty-five minutes of staring at a resolver error that made no sense.
Since host networking nails the pod to a specific machine's network identity anyway, the deployment leans in. Node affinity requires the master node, with a matching toleration for its taint. Portability was already spent, so determinism was worth buying with it. If the cluster ever grows a second node, Home Assistant will not be invited to wander.
A busybox init container as the bootstrapper
My favorite piece of this manifest is the tiny shell script that runs before Home Assistant starts. The problem: Home Assistant owns and rewrites its configuration.yaml at runtime, so you can't mount the whole config from a ConfigMap read-only. But a fresh install still needs sane initial config, and running behind Traefik requires a proxy block Home Assistant won't add itself.
The compromise is a busybox init container with two idempotent moves. If configuration.yaml doesn't exist, seed it from the ConfigMap template. If it exists but lacks an http: section, append the reverse-proxy settings: use_x_forwarded_for plus the cluster's pod CIDR (10.42.0.0/16, the k3s default) as a trusted proxy, so Home Assistant believes the client IPs Traefik forwards instead of treating every login as an attack from inside the house.
The template itself stays deliberately minimal. The default_config integrations, text-to-speech wired to a translate service, and that proxy block. Everything else, the dashboards and automations and device configs, belongs to the app's own runtime mutations. It's ConfigMap-as-seed rather than ConfigMap-as-truth. Git owns the starting state and the invariants, the app owns what it learns afterward, and the init container is the boundary negotiator between GitOps and software that was never told about GitOps. Ten lines of shell doing the diplomacy.
The idempotency is what makes it live comfortably under Argo CD. Every pod restart re-runs the init container, and on a healthy volume both checks no-op in milliseconds. There's no "first install" flag anywhere, no state about whether bootstrapping happened. The script just converges the volume toward its invariants every single boot.
Don't record whether you did the thing. Check whether the thing is true.
The Matter sidecar
Matter device support needs a separate controller process, and this is where Kubernetes stops fighting me and starts helping. The Matter server rides in the same pod as a second container. Sidecar semantics are exactly right for once. Commissioning a Matter device involves the controller and the hub coordinating over the local network, and sharing the pod's host network means they see the same network reality by construction. They share a lifecycle so they restart together, the Matter server keeps its own small volume, and there is no scenario where one is up, the other is down, and I'm debugging which. The pattern that felt like ceremony for web apps is genuinely the correct shape here.
Probes for software that boots like a house
Health checking stays humble. TCP probes on the web port, liveness with a 60-second initial delay and 30-second period, readiness at 30 and 10. Home Assistant boots like it's stretching first. Integrations initialize serially, some wait on network devices that may themselves be waking up, and the whole process takes longer after a power cut, which is precisely when you least want Kubernetes making things worse.
That's why the probes answer "is the process up" and nothing deeper. Every increment of probe cleverness, checking the API, checking an integration, is another way for a slow Zigbee stick to get the pod killed mid-boot, restarted, killed again, forever. I've watched that loop. On restarts-after-power-cuts, patience beats precision, and a liveness probe should test for "alive", not "fully dressed".
Verdict
Would I recommend this to someone who just wants working lights? No. Buy the appliance, enjoy your evening. The honest benefit case is narrower and better. If a k3s cluster already runs your house, folding Home Assistant into it means the smart home gets the same superpowers as everything else. Config in Git, upgrades by bumping an image tag in a commit, rollbacks by revert, rebuild-from-scratch as a twenty-minute plan rather than a lost weekend.
The pod pins to one node, claims its network, refuses replicas, and mutates its own config. Kubernetes purists would wince. But it's been quietly running the house for months, surviving upgrades and power cuts, and every workaround it needed is documented in the one place I'll actually find it again: the manifest.
References
- Home Assistant: The open-source smart home platform at the center of the post: it speaks to thousands of device types locally and keeps automation in the house instead of a vendor cloud. Architecturally it's a stateful singleton, which is precisely what makes running it on Kubernetes interesting.
- python-matter-server: Home Assistant's controller implementation for Matter, the cross-vendor smart home standard. It must run as its own process, which is why it rides as a sidecar container sharing the pod's network and lifecycle.
- Kubernetes deployment strategies: The docs behind the
Recreatechoice. Rolling updates overlap old and new pods for zero downtime; Recreate fully stops the old pod first. For software with an exclusive-access SQLite database, overlap means corruption, so the 'worse' strategy is the correct one. - Kubernetes init containers: Containers that run to completion before the main containers start. Here a busybox init container seeds and patches configuration, solving the mismatch between GitOps (Git owns config) and Home Assistant (the app rewrites its own config) by giving each a defined territory.
- Traefik: The k3s-bundled ingress that fronts the web UI. Its involvement is why the trusted-proxy block must be appended to Home Assistant's config, so forwarded client IPs are believed rather than treated as spoofing.