
🌐 Also available in: Deutsch · Français · Español
😌 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.x, 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 doterraform 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.x) — 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
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 user3 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.




