Every home lab is the best lab in town, as long as you don’t go to town. Mine fits in a shoebox, sips power, and runs everything from my git server to the cameras. Here are the decisions behind it, mostly so future me remembers why.
Hardware: Turing Pi 2 + three RK1s
A Turing Pi 2 board with three RK1 modules (Rockchip RK3588, 8 ARM cores, 16 or 32 GB RAM) and an NVMe drive per node. It’s silent, small and cheap to run 24/7, which matters more at home than raw speed.
Every node is both control plane and worker. Three nodes means etcd can lose one and keep going, and I don’t have a fourth board to spend on “just control plane”.
Lesson learned the hard way: the onboard eMMC is not a system disk. One node started throwing I/O errors and crash-looping etcd twice in an hour. All three now boot Talos from NVMe, and the eMMC sits there doing nothing.
OS: Talos Linux
No SSH, no shell, no package manager. The whole OS is one config file and an API. Nodes are cattle, even when there are only three of them.
Extensions are baked into the image from Talos’ Image Factory: iSCSI tools for Longhorn, the Rockchip NPU driver, and gVisor for workloads I don’t fully trust, like sandboxes for AI agents. Upgrading a node is one command, and it either comes back exactly as configured or it doesn’t come back.
GitOps: Argo CD, app of apps
Everything lives in one repo. A root Argo CD Application points at a folder of Applications, and sync waves order them: CRDs first, then controllers, then their config, then the apps.
This cluster replaced an older k3s + Flux one, moved over app by app. k3s was the reason for the move: it wasn’t stable for me and kept failing during upgrades. Flux was fine, honestly. I swapped it for Argo CD mostly because I wanted to learn Argo properly, and running it at home is the best way to do that.
One surprise: selfHeal means what it says. kubectl scale --replicas=0 to
copy some data? Argo puts it back within seconds. The only way to pause a
workload is to commit the change. Annoying the first time, correct every
time after.
Storage: Longhorn + MinIO on separate disks
Longhorn replicates every volume across all three NVMe drives. A node can die and the volumes don’t care.
Backups go to MinIO on two separate SATA SSDs on one node. They are deliberately not in the Longhorn pool: if the main storage breaks, I don’t want the backups to break with it.
Databases: CloudNativePG
One Postgres cluster run by CloudNativePG, and every app gets its own role and database, declared in git like everything else. Continuous backups go to MinIO, plus a copy to the NAS. Upgrades, failover and backups are the operator’s problem, not mine.
Ingress: Envoy Gateway
One Envoy Gateway on one LoadBalancer IP (MetalLB), using the Gateway API. It also handles plain TCP, so Postgres goes through the same front door.
People log in through Authentik SSO. It’s my third attempt, after Authelia and Zitadel were installed and then fully removed. Machine APIs skip SSO and use an API key checked by the gateway itself.
DNS and certificates: Cloudflare
Cloudflare hosts the gskode.com zone. cert-manager gets a Let’s Encrypt
wildcard through DNS-01 with a scoped API token, so there’s no port 80 to open.
The trick I like: public DNS points *.gskode.com at a private IP. Valid
certificates and real hostnames everywhere, but it only works from home or
over VPN. Nothing in the lab is reachable from the internet.
That’s also why this blog doesn’t run there. It’s static files on Cloudflare’s edge. The lab stays private, and the one public thing lives somewhere built to be public.
Secrets: External Secrets Operator
One rule: no secret is created with kubectl or committed to git. Secrets are
seeded once into one namespace, and External Secrets copies them where they’re
needed. Apps never know where a secret really comes from, so the backend can change
without touching them.
That’s where I want to go next. I keep my passwords in Proton Pass, and its CLI is finally good enough to build on. I’m thinking about writing an External Secrets provider for it, so the password manager I already use becomes the source of truth for the cluster too.
What runs on it
Forgejo with its own CI runner, Zot as the container registry, Prometheus, Grafana and Tempo, Frigate for the cameras, EMQX as the MQTT broker, and a growing pile of small tools I write for my AI agents.
Next
Flannel doesn’t enforce NetworkPolicies, so the policies I have are decoration. Cilium is next, mostly to isolate the AI agent sandboxes. gVisor keeps an agent away from the node’s kernel. Cilium should keep it away from everything else on the network. Hubble’s view of who talks to whom is a nice bonus.
Best lab in town. Population: me.