Theme

Homelab field notes

I accidentally built a tiny CI/CD platform

A case-study style walkthrough of how a Debian control plane, Pimox app workers, external Gitea, local registry, Kyverno policy, Argo CD, monitoring, and static demo shelf became a repeatable Kubernetes delivery path.

Portfolio case studies

Production-shaped evidence

These are the three proof points a hiring manager should see first: platform ownership, production reliability at scale, and the reserved MLOps path for model-serving work.

Case 01

Self-hosted Kubernetes delivery platform

Git push, validation, image build, registry, GitOps sync, policy guardrails, monitoring, retained storage, and VM worker provisioning in one small but operationally honest platform.

View architecture
Case 02

Enterprise SRE and incident automation

Oracle and prior enterprise roles show the production side: 20,000+ developer users, 10,000+ external customers, Linux troubleshooting, automation, runbooks, on-call improvement, and high-scale incident response.

View CV evidence
Case 03

MLOps deployment platform placeholder

Reserved for the next serious demo: FastAPI inference, Kubernetes manifests, rollout strategy, model metrics, drift signals, and rollback behavior.

Open placeholder

Architecture map

The homelab, end to end

The current delivery path starts with a push to Gitea, runs local validation, builds arm64 images, syncs the validated commit into the GitOps mirror, and lets Argo CD reconcile from the app workers. The infrastructure path stays manual through lab.sh, including the PXE/Pimox template builder, NVMe-backed worker clones, Kyverno policy placement, and the opt-in OpenWrt firewall VM, while the OCI edge routes public traffic back through the private path.

Homelab architecture map Git push enters Gitea, Gitea Actions validates and builds app images, OpenTofu manages cluster and provisioning layers, Debian keeps the control plane and PXE services, Pimox app workers run Argo CD, Kyverno, and app workloads on NVMe-backed VMs, OpenWrt can run as an opt-in firewall VM, and the OCI edge routes traffic into Kubernetes services. Source, validation, and images Control plane and provisioning Workers, edge, and workloads Developer laptop edit, test, push main Gitea repository https://lab2025.duckdns.org/git/ main is the release branch Gitea Actions runner Debian hosted runner runs validated deploys Validation gates Gitleaks secret scan Trivy IaC and image posture Buildx image build linux/arm64 website + demos OpenTofu + lab.sh manual infra apply path apps command for CI deploys PXE + preseed service dnsmasq TFTP, nginx HTTP Debian 13 arm64 netboot golden-node prep scripts kubeadm control plane API server and control loops workloads pushed to app workers GitOps mirror validated commit copied locally Argo CD reads deploy state GitOps + policy controllers Argo CD and Kyverno pinned to app workers Monitoring stack Prometheus, Grafana, Loki Promtail, node-exporter, KSM Storage and backups OpenEBS retained PVs Gitea dumps and monitoring data OCI edge host nginx, HAProxy, Varnish, Squid TLS, routing, caching public DNS entry point Tailscale + edge routes 30080 website, 30081 demos 3000 Gitea on Raspberry Pi Raspberry Pi 192.168.100.89 external Gitea Docker service optional edge-app worker repo home and backup source Orange Pi 5 Plus Pimox pimox-worker app nodes workers on nvme_thin_pool Argo CD, Kyverno, apps idempotent qm automation OpenWrt firewall VM VM 9050, opt-in only vmbr0 WAN, vmbr1 LAN simple firewall path DHCP optional, VLANs later Local registry :30500 php-website and demos-static pulled by app workers push workflow scan build manual infra validated Git serve boot join path Pimox template firewall VM policy + GitOps secure tunnel service traffic image pulls

The diagram is intentionally operational: it shows the app delivery loop, image flow, provisioning path, worker-placement boundary, monitoring layer, OpenWrt firewall option, and public traffic path without hiding the practical bits that make a small lab behave like a platform.

Open the Christmas-tree version
Future me, judging

Be honest: why build all this instead of just running a couple containers like a normal person?

Me, holding coffee

Because apparently I looked at "host a website" and thought, "what if this had a control plane, GitOps, retained storage, an image registry, and several new ways to embarrass myself?" The real goal was practice: provision the infra, keep config in Git, deploy with automation, break it, fix it, and make sure I could rebuild it without relying on shell history and vibes.

Future me, judging

Why kubeadm? Were managed clusters too emotionally stable?

Me, holding coffee

Pretty much. kubeadm keeps the cluster close to the metal, which is a polite way of saying I get to see every sharp edge. The Debian node runs the control plane, the Raspberry Pi joins as an arm64 worker, and Pimox on an Orange Pi 5 Plus now gives me a repeatable way to add Debian 13 arm64 VM workers. Suddenly networking, storage, container runtimes, certs, and node recovery are not mysterious cloud magic. They are my problem.

Future me, judging

So where is the CI/CD part hiding?

Me, holding coffee

It is small, but it is real. OpenTofu brings up the cluster, platform, apps, and edge layers. Argo CD watches Git from the app workers, Kyverno keeps policy pressure on the workloads, Docker Buildx builds linux/arm64 images, and the local registry feeds the cluster. No enterprise dashboard fireworks, just a clean loop: Git changed, image built, cluster updated, nobody had to kubectl-edit anything at 2 AM.

Future me, judging

Why run your own registry and Gitea? Was the simple option unavailable?

Me, holding coffee

The simple option was very available, which is why I heroically ignored it. The registry means experiments do not need to go to a public image repo, and external Gitea gives the lab its own Git service without making Kubernetes responsible for its own source of truth. Together they make the setup feel less like "some containers under the stairs" and more like a tiny platform with opinions, responsibilities, and occasionally dramatic storage needs.

Future me, judging

What actually hurt the most?

Me, holding coffee

Storage. Always storage. Kubernetes, Docker, retained volumes, and build caches can fill a small root disk with the quiet confidence of a bad decision. Moving OpenEBS local volumes and Docker data to the external SSD turned the lab from "why is everything on fire?" into "okay, this is usable now." Growth, allegedly.

Future me, judging

So the platform controllers finally moved off the control plane?

Me, holding coffee

Yes. Argo CD and Kyverno now target the homelab.dev/node-role=app workers, including Kyverno hook jobs, so the Debian node can stay focused on control-plane duties. That one change made the lab feel less like everything was balanced on the first machine that happened to boot.

Future me, judging

Can the current cluster actually handle all that, or are we about to smoke the Pi?

Me, holding coffee

The Pi survives because the demos are intentionally local-first and now ship as a separate static artifact. The website pod stays a portfolio shell, the demos-static pod serves static bundles, and the user browser does the expensive work. If I later ship real ONNX object detection, Transformers.js, or full video transcoding models, those must lazy-load in the browser or move to a beefier node. The Raspberry Pi is brave, but it is not a GPU wearing a tiny hat.

Future me, judging

So the lab can now build its own worker nodes?

Me, holding coffee

Yes, and now with fewer crossed fingers. Debian runs a provisioning layer with dnsmasq, nginx, PXE boot files, GRUB, and a Debian 13 arm64 preseed. OpenTofu talks to Pimox through qm, creates VM 9000 on local storage, boots it from the network, installs the OS, runs golden-node prep, disables swap, verifies cgroups, installs containerd and kubeadm tooling, then seals it as a template. Worker clones are idempotent by VMID and now land on nvme_thin_pool, so local storage stays reserved for the template.

Future me, judging

And OpenWrt is joining the story too?

Me, holding coffee

Only as a simple firewall, not as a networking science project. The pipeline can create an opt-in OpenWrt ARM SystemReady VM, attach vmbr0 as WAN and vmbr1 as LAN, and configure the LAN side without rewriting Orange Pi host networking. DHCP stays optional, and VLANs wait until there is a managed switch and a local test window.

Future me, judging

What changed on the observability and scheduling side?

Me, holding coffee

Monitoring moved from "someday" to "running," and scheduling moved from "whatever fits" to explicit worker placement. Prometheus Stack, Grafana, Loki, Promtail, node-exporter, and kube-state-metrics give the lab useful signals; the next useful step is choosing the few alerts that would actually wake me up for the right reasons.

Recent activity log

What changed since the first build

The lab moved from a working Kubernetes experiment into a more complete self-hosted delivery system. The latest work focused on trust, repeatability, VM-based expansion, controller placement, and making deploys match the exact commit that passed validation.

  1. 01 Moved Gitea out of Kubernetes and onto the Raspberry Pi as the local Git service, while keeping the public /git/ route through the edge stack.
  2. 02 Installed and validated a Debian-hosted Gitea Actions runner so pushes to main can build, scan, and deploy without depending on a laptop session.
  3. 03 Added a custom checkout flow for the /git/ subpath and kept a persistent Debian checkout for the deployment scripts.
  4. 04 Added Gitleaks secret scanning and Trivy scanning for the app and infrastructure tree.
  5. 05 Changed deployment so the validated commit is pushed into the local GitOps mirror before lab.sh runs, preventing Argo CD from reconciling an older tree.
  6. 06 Hardened the website, demos-static, and registry workloads with non-root containers, read-only root filesystems, resource limits, and explicit writable volumes.
  7. 07 Split the demos into a dedicated demos-static image and Argo CD application so the PHP website stays small and boring.
  8. 08 Changed Gitea backups to dump from the Raspberry Pi Docker container and store archives on the Debian host.
  9. 09 Validated the full main-branch deployment path: fetch main, apply OpenTofu layers, build and push arm64 images, refresh Argo CD, and confirm the runner completes successfully.
  10. 10 Built the Debian 13 arm64 Pimox template end to end with PXE, preseed, qemu-guest-agent discovery, cgroup validation, swap disabled, and a final seal step.
  11. 11 Added NVMe-backed Pimox worker clone automation so VM 9000 stays on local storage while worker nodes are created on nvme_thin_pool.
  12. 12 Added an opt-in OpenWrt VM path for a simple firewall between vmbr0 and vmbr1, with guardrails that avoid Orange Pi host networking changes.
  13. 13 Installed the monitoring stack and moved platform add-ons such as Argo CD, Kyverno, and prometheus-stack work toward app-worker placement instead of treating the control plane as spare capacity.

Technologies and why they are here

Improvement backlog

Todo list for the next homelab pass

These are improvement proposals, not chores for the sake of chores. Each item either reduces rebuild risk, tightens supply-chain hygiene, or makes the platform easier to operate when something fails.

Visitor ideas

What would you improve next?

Send a practical idea for the homelab backlog. Submissions are stored as plain text, limited in size, and rendered escaped.