Homelab Platform

I wanted somewhere to write. I built a production platform instead.

No scaling requirements. No users. No revenue. So naturally I built a three-node Kubernetes cluster on bare metal, automated every layer of it, and exposed it to the internet through a zero-trust tunnel with no open ports.

The blog was never the point. The point was learning how real platforms are actually built — not tutorials, not “hello world” deployments, but the systems that sit behind production environments. So I treated a personal site like one: fully automated, reproducible, observable, and secure by design.

The page you are reading is served by it.


One command

docker compose up

That provisions the infrastructure, configures the cluster, deploys the platform services and applications, and sets up public access. About 25 minutes later the whole platform is live. No manual steps, and no SSH after bootstrap.

Git → CI/CD → Terraform → VMs → Ansible → Kubernetes → Argo CD → Apps
  • Terraform creates the VMs on Proxmox and configures Cloudflare DNS and the tunnel.
  • Ansible bootstraps Kubernetes with kubeadm and installs the platform services in dependency order.
  • Argo CD takes over from there, reconciling workloads against Git continuously.

Both Terraform and Ansible run in containers, so the build executes identically on a laptop and on the CI runner. No version drift, no host prerequisites beyond Docker.


What runs on it

  • This website — WordPress and MariaDB on persistent storage, with nightly encrypted backups.
  • Prometheus, Grafana and Alertmanager — cluster and node metrics, with alerts by email.
  • Argo CD — the GitOps control plane, itself deployed by the pipeline it then takes over from.

GitOps changed how I think about it

I change a file, push, and Argo CD updates the cluster. No kubectl, no manual deploys. That shift — from running commands to declaring desired state — is the single biggest change in how I approach systems.

It also has a boundary that took a failure to understand properly. GitOps reconciles declarations, not data. Git guarantees the platform can be rebuilt; it guarantees nothing about what is running on it. That is why the first thing I built on this platform was backups — before there was any data worth losing.


What went wrong

It did not work first time. Terraform not returning VM IPs. Ansible running before infrastructure was ready. Probes killing WordPress before it stabilised. Infinite HTTPS redirect loops behind Cloudflare. Storage deadlocks during a routine update.

Nearly all of it came down to one thing: I assumed things would be ready when they were not.

Stop designing systems that depend on timing. Start designing systems that converge.

Instead of “wait 30 seconds and hope”: retry until ready, check actual state, design for failure. That shift alone made everything more reliable.

Every significant failure is written up in full — symptom, diagnostic commands, the hypotheses that were wrong, and the root cause. The detailed engineering analysis lives on the Kubernetes Platform page.


The real win: reproducibility

The entire platform can be destroyed, rebuilt and moved from the same codebase. No snowflake servers, no hidden configuration, no “it only works on my machine”.

Yes, this is overkill for a blog. That is rather the point — if you can build it for something that does not matter, you can build it for something that does.