Building a Homelab
Since 2024 I had this idea of running a homelab at home. A real Kubernetes cluster where I could test anything, break things, rebuild, and keep it running 24/7. I watched a lot of YouTube videos about it, read blog posts, and kept thinking "one day."
In the meantime I used kind, k3d, and minikube for everything. They're great for learning, but they're ephemeral. Every time I closed the laptop, the cluster was gone. No persistence, no real GitOps, no long-running workloads. I wanted something permanent.
In early 2026 I finally went for it. DDR5 memory prices were absurd because of AI demand, but I decided to just deal with it and buy the hardware anyway.
Hardware
I went with a single mini PC instead of multiple nodes. More memory in one machine, and if I need more disk later, I just add it.
- Ryzen 9
- 96GB DDR5 5600MHz
- 1TB NVMe
Expensive, especially the memory. But I wanted room to run a real stack without constantly hitting limits.
On top of it I installed Proxmox, so the Kubernetes nodes run as virtual machines. This gives me snapshots, easy rebuilds, and the flexibility to spin up other VMs or LXC containers for things outside the cluster (like AdGuard for DNS).
Two clusters: develop and production
I started with one cluster and it broke down fast. Testing a chart change meant testing it on the same cluster running my long-lived workloads. Restart Prometheus to try a new config, and the dashboards I was watching went down with it.
So I split it the way most companies do: a develop environment to try things, a production environment that stays stable. Same mini PC, two independent clusters.
flowchart LR
subgraph PVE["one mini PC, Proxmox"]
D["develop<br>cp + 3 workers (s / m / l)"]
P["production<br>cp + 3 workers (s / m / l)"]
end
D --> DD["*.dev.ruiz.sh"]
P --> PP["*.prd.ruiz.sh"]
Each cluster is four nodes: one control plane and three workers split into small, medium, and large tiers, so scheduling decisions actually mean something. They share nothing. I can break develop without production noticing, which is the whole point.
Built with Terraform
Terraform provisions both clusters from the same code, on Talos Linux. The only difference between them is sizing. It runs in three stages, each an independent init/plan/apply:
flowchart TD
H["host<br>put the Talos image on Proxmox"] --> C["cluster<br>create VMs, apply Talos config, bootstrap k8s"]
C -->|kubeconfig| B["bootstrap<br>install MetalLB + ArgoCD, hand off to GitOps"]
They're separate because each stage depends on the one before it. cluster needs the image on the host; bootstrap needs the kubeconfig the cluster produces. Splitting them lets me rebuild a cluster without re-running the bootstrap, or the other way around.
Talos has no SSH and no shell. The whole node is configured declaratively through a machine config that Terraform applies: hostname, network, and the worker tier label. Terraform creates the VMs, applies that config, bootstraps the cluster, and outputs a kubeconfig. No manual steps in between.
What Terraform installs
Almost nothing. The bootstrap stage installs only what has to exist before GitOps can take over:
- MetalLB. Gives LoadBalancer services an IP to hand out.
- ArgoCD. Minimal config; it manages itself from Git afterward.
- Secrets. Repo and Doppler credentials so ArgoCD can start syncing.
MetalLB goes first, in L2 mode (no BGP, it's a flat home LAN). Without it, every LoadBalancer service sits pending with no IP to assign.
ArgoCD comes next, with the thinnest config I can get away with: admin password, repo access, node placement. I keep it deliberately minimal, because the first thing ArgoCD does is adopt and manage its own config from Git. The Helm release is marked ignore_changes = all so Terraform stops touching it after install.
Secrets are what make the handoff work, and Terraform never stores them. Every command is wrapped with Doppler, which injects credentials as TF_VAR_* at runtime:
doppler run --project homelab --config pve -- terraform apply
From those, the bootstrap creates two Kubernetes secrets: GitHub credentials so ArgoCD can clone the config repo, and a Doppler token so the cluster can pull its own secrets later. Once ArgoCD is running and pointed at the repo, Terraform's job is done. Everything else is GitOps, which I'll cover in upcoming posts.