Ramblings of an aging IT geek
← Ramblings of an aging IT geek
homelab

running kubernetes at home was mostly a mistake

After two years of running a real Kubernetes cluster in my homelab, an honest accounting of what it cost me and the narrow case where it was worth it.

A homelab server rack with blinking lights

I ran a proper Kubernetes cluster in my homelab for about two years. Three control-plane nodes, a few workers, the whole apparatus. I am writing this the week I tore most of it down, and I want to be honest about it: for what I actually do at home, Kubernetes was mostly a mistake. Not entirely. But mostly. And the "mostly" is the interesting part, so stick around for it.

Let me be clear about who this is and isn't for. If you run Kubernetes at work, learning it deeply at home is a legitimate, even excellent, reason to do this. That was genuinely part of my justification and I do not regret the education. What I regret is pretending the cluster was the right tool for hosting my actual services, because it wasn't, and I spent a lot of evenings maintaining that fiction.

what I was actually running

The honest inventory of what lived on this cluster: a couple of databases, Home Assistant, a Git server, some monitoring, a media stack, a few personal web apps, and the usual assortment of things that listen on a port and occasionally need a restart. Maybe fifteen workloads. None of them needed to scale. None of them needed to survive a node failure without me noticing. If a service was down for ten minutes while I ate dinner, the consequences were precisely nil.

That last point is the crux. Kubernetes is, at its heart, a system for keeping declared state true without human intervention, across failures, at scale. I had no scale, my failures were rare, and I am the human, sitting right there, perfectly happy to intervene. I had bought a self-healing distributed orchestrator to run things that mostly just needed to come back up after a reboot.

A homelab shelf with mixed networking and compute gear

where the time actually went

Here is the part people skip when they evangelise homelab Kubernetes. The workloads were never the hard bit. The platform was. A representative sample of evenings I will not get back:

  • Certificate renewals quietly failing because cert-manager's CRDs lagged behind a control-plane upgrade.
  • An etcd that got unhappy after an unclean power event, because my UPS runtime did not cover a long enough outage, and recovering it was a genuinely tense hour.
  • CNI upgrades that turned pod networking into a coin toss until I matched versions exactly.
  • Storage. Always storage. Persistent volumes in a homelab are where dreams go to deadlock.
  • Upgrades themselves. A single-version Kubernetes upgrade is not a casual apt upgrade. It is a project with a changelog you must actually read.

None of these were Kubernetes being broken. They were Kubernetes being Kubernetes: a serious distributed system with serious failure modes, asking for the operational diligence it deserves. The problem was that I was paying that operational tax to host Home Assistant. The ratio of platform maintenance to actual benefit was, frankly, embarrassing once I added it up.

the rebuild

So I moved nearly everything to plain Docker Compose on two well-backed-up hosts, with a reverse proxy in front and a tidy set of systemd timers for the housekeeping. The entire migration took a long weekend. The result starts faster, fails in ways I understand instantly, and when something breaks the debugging is docker logs and a glance at the compose file, not a journey through three layers of abstraction to discover a pod is stuck Pending because of a taint I forgot I'd applied.

A compose file you can read in one screen has a real, underrated virtue: the entire deployment is right there. No controllers reconciling state behind your back, no operators with their own opinions, no kubectl describe archaeology. The thing you wrote is the thing that runs.

services:
  app:
    image: ghcr.io/example/app:1.4.2
    restart: unless-stopped
    ports:
      - "8080:8080"
    volumes:
      - ./data:/data
    environment:
      - DATABASE_URL=postgres://app@db/app

That is the whole deployment for that service. Eleven lines. I know exactly what it does. When it misbehaves I know exactly where to look. For a homelab, that legibility is worth more than every scheduling feature I gave up.

The same homelab from a different angle, cabling visible

the part that wasn't a mistake

Now the "mostly". A small slice genuinely benefited, and I kept a tiny single-node k3s install for it.

The first reason is the education, and I will not undersell it. Operating Kubernetes at home, breaking it, and fixing it at three in the afternoon with nothing on fire taught me things that paid off directly at work. You cannot truly learn how etcd behaves under a dirty shutdown from a tutorial. You learn it by living through it on hardware you own, where the only thing at stake is your own evening.

The second is that a couple of workloads really did suit the model: things I wanted to redeploy from a Git repository on every change, where having the declarative manifest be the source of truth was the point rather than overhead. For those, a minimal k3s plus a GitOps controller is a clean, pleasant way to work, and I left it running.

So the honest conclusion is not "Kubernetes bad". It is "match the tool to the actual problem, and at home my actual problem was small". I let the interesting technology choose the workload instead of letting the workload choose the technology, and I dressed it up as architecture. Two years of evenings later, the cluster is mostly gone, the services are more reliable, and I sleep better. The k3s node that remains earns its place. The three-node control plane never really did, however much I enjoyed pretending it might.

If you are about to build a serious cluster to run a dozen hobby services, do it with both eyes open. Do it to learn, by all means. Just do not tell yourself it is the simplest way to keep Home Assistant online, because a two-line restart: unless-stopped was always going to win that fight.