
π Fair warning: this installment is the calm before the storm. GitLab and the Seed VM turned out to be refreshingly easy β boring infrastructure, done boringly, on purpose. I’m mentioning that explicitly because the next post is Kubernetes territory β FluxCD, GitOps reconciliation loops, and all the ways a cluster can look healthy right up until it very much isn’t β and that one was a considerably bigger fight. Consider this the deep breath before that dive. π€Ώ
π¦ Every homelab reaches a point where “I’ll just SSH in and run the command by hand” stops being charming and starts being a liability. This post is about the moment this lab crossed that line: standing up GitLab as the single source of truth, and building a dedicated Seed VM so that “the tools to manage the infrastructure” stop living on whatever laptop I happen to be holding. π»β‘οΈποΈ
Catch up on the earlier episode here: Homelab Rebuild 2026: Building the Proxmox Cluster and Provisioning it with Terraform.
π¦ GitLab: From a Bare-Metal Box to a Proper VM
GitLab has actually been running in this homelab for a while now β just not correctly. The original instance lived directly on HP3’s bare metal (Debian, no hypervisor underneath, no snapshots, no backups worth the name). Functional, but exactly the kind of “temporary” setup that quietly becomes permanent infrastructure everyone depends on. π
The fix: install GitLab properly as a VM (HP1-gitlab01, 192.168.10.32, 12 GB RAM, 120 GB disk) via the standard Omnibus package β GitLab’s official apt repository, one package containing GitLab itself, its bundled PostgreSQL, Redis, and NGINX, configured through a single /etc/gitlab/gitlab.rb. It’s the boring, well-trodden install path on purpose: this is infrastructure I want to not think about, not a science project. Internally it’s plain HTTP, no TLS in front of it yet β fine for a LAN-only instance, though it did come back to bite me later when FluxCD’s bootstrap refused to send credentials over unencrypted HTTP (a story for another post π).
The actually interesting part wasn’t the fresh install β it was getting the old instance’s data across without losing history. GitLab’s own backup/restore tooling handled it: a full gitlab-backup create on the old bare-metal box, the resulting archive plus the instance’s secrets file (gitlab-secrets.json) copied over, then gitlab-backup restore on the new VM against a matching GitLab version. Projects, issues, and β critically β every branch of every repo, including this project’s internal branch (the one holding actual credentials, never pushed to the public GitHub mirror) came across intact. I had Claude walk the whole export β transfer β import β verify sequence and confirm the migrated instance genuinely matched the old one before HP3 got wiped. πβ
Once verified, HP3’s bare-metal Debian install was wiped entirely and rebuilt as a third Proxmox VE node β meaning GitLab isn’t just on a VM now, it’s on a VM sitting in the same Proxmox cluster as everything else, with snapshots and PBS backups like any other workload. No more “hope the bare-metal box doesn’t die” energy. π
π± Enter the Seed VM
With GitLab holding the actual source of truth for Terraform and Ansible, the next gap was obvious: where do terraform apply and ansible-playbook actually run from? “Whatever machine has the tools installed that day” is not an answer, it’s a liability wearing a trenchcoat.
So: HP1-seed01 (192.168.10.33) β a small, boring 4 GB / 60 GB Ubuntu VM whose entire job is being the one place infrastructure changes get made from. Fully built by an Ansible playbook (02_ansible/playbooks/seed01.yml), so it’s rebuildable from scratch in minutes, not “however I remember setting it up last time.” π±
What lands on it:
- π§ git β no version pin, no reason to be precious about it
- ποΈ Terraform, pinned to an exact version (
1.15.8) via HashiCorp’s official apt repo β infrastructure changes should never silently pick up a new Terraform release mid-apply - π Ansible itself, so the seed VM can eventually run playbooks against the rest of the fleet, itself included
- βΈοΈ kubectl, pinned to the RKE2 cluster’s minor version (currently tracking
v1.36) via the official Kubernetes apt repo - β Helm, pinned to an exact release, pulled straight from
get.helm.sh
Two nice details worth calling out. First, the playbook doesn’t just drop a bare kubectl binary and call it done β it fetches the live kubeconfig directly off the cluster’s first control-plane node, rewrites the loopback address to that node’s real IP, and installs it for both the ubuntu and raphael accounts. kubectl get nodes works the second the playbook finishes, no manual copy-pasting a kubeconfig around. Second: every account gets a k alias for kubectl wired up with full bash completion β so k get pods tab-completes exactly like kubectl get pods would. Small thing, saves keystrokes forever. β¨οΈ
The versions themselves live in one place (group_vars/all.yml) β bump a number, re-run the playbook, every tool upgrades in lockstep with whatever the cluster actually needs. No more “wait, which kubectl minor version am I even running” moments.
π§ Why This Matters More Than It Sounds
None of this is glamorous. Nobody writes a blog post titled “I installed kubectl” and expects applause. But it’s the difference between a homelab that’s a museum of one-off manual steps I’ll have forgotten in six months, and one where “rebuild everything from Git” is a real, tested statement rather than a hopeful one. GitLab is now the actual source of truth; the Seed VM is the actual place changes get applied from. Two boring VMs, doing exactly one job each, both fully reproducible from code. π¦
Coming up: either the Kubernetes/FluxCD/Longhorn side of this cluster, or β if I finally crack it β the ongoing mystery of the domain-joined Windows client whose user-scope Group Policy flatly refuses to apply a single GPO despite Active Directory itself checking out perfectly healthy from every angle I’ve thrown at it. Stay tuned. π΅οΈββοΈ





