Homelab platform
The Kubernetes platform most of my projects run on.
- Built with
- Proxmox, Ceph, TrueNAS SCALE, Sidero Omni, Talos Linux, Traefik, CloudNativePG, Gitea, MinIO, Coder, Keycloak, Technitium DNS
- Kubernetes on Talos Linux, managed by Omni, with Traefik out front.
- My own Gitea and private container registry, so my git hosting is mine too.
- Coder for dev environments on demand.
It’s where I test ideas before trusting them at work. Most fail, and that’s the point.
Hardware
- A 9-node Proxmox cluster with mixed Intel and AMD nodes. Three nodes on bonded 10G carry the fast storage tier; the rest are small, efficient boxes on 1G.
- Ceph with two separate tiers: fast SSDs for VMs, and bulk drives for everything else.
- TrueNAS SCALE hosts the Talos VMs and serves iSCSI and NFS volumes to Kubernetes.
- UniFi networking with VLANs for services, storage and an isolated segment for the cluster.
Clusters
- Management cluster: a single Talos node that runs Omni and Keycloak. I moved them off two hand-built VMs without losing Omni’s state, so the existing clusters reconnected without being re-adopted. It’s deliberately not managed by Omni itself.
- Homelab cluster: one control plane and three workers, defined as an Omni cluster template in git.
- Omni’s Proxmox provider can create Talos nodes on demand. I’ve used it to build and tear down throwaway clusters.
What runs on it
- Gitea: two active-active replicas with Postgres, shared storage and a Dragonfly cache, with Actions and a private container registry. It hosts all my repos.
- CloudNativePG: a three-instance Postgres 17 cluster behind Gitea, Coder and Pit Wall.
- MinIO for object storage: S3 API and console on one hostname.
- Coder for dev environments, and Obsidian in the browser for notes.
- Moonlight for game streaming in the browser.
- This site, and Pit Wall.
- Platform add-ons: TrueNAS CSI drivers, MetalLB, a descheduler, and the NetBird operator that puts it all on the mesh.
- DNS: a Technitium pair on two separate physical machines, replicating zone changes in about 10 seconds.
How things get deployed
- Cluster config, Helm values and manifests all live in git and are applied deliberately. I tried GitOps controllers elsewhere and they cost more than they gave at this scale.
- Apps build in Gitea Actions on a custom runner image. Each merge pushes an image tagged with its commit and rolls it out with a namespace-scoped service account. If the new pods never become ready, the job fails.
Things it taught me
- A $15 transceiver can take down a cluster. A failing optic in a round-robin bond made a node fence itself six times. Every bond is LACP now.
- HTTP/2 connection reuse can break logins. A wildcard certificate let the browser reuse a connection for a different host, and single sign-on returned 404. A certificate scoped to the right hostnames fixed it.
- More CPUs can crash a pod. Dragonfly starts a thread per core and needs memory for each, so giving its node more vCPUs put it in a crash loop. Pinning the thread count fixed it.
- DNS replication can fail quietly. The primary’s NOTIFY messages came from a NAT’d address, so the secondary refused them and fell back to slow polling. Nothing looked broken.
- Changing one bridge can drop every VM. Editing any interface through the NAS’s middleware re-applied the main bridge and disconnected all VMs at once.