A real, self-hosted GitOps Kubernetes platform running on my own Proxmox hardware — Terraform → Ansible → K3s → ArgoCD, with Sealed Secrets, Prometheus/Alertmanager alerting to my phone, and a CI pipeline that validates every manifest before it reaches the cluster. Every bug hit along the way is documented with its root cause, not just the fix. Started as a cloud DevOps portfolio project on Oracle Cloud Infrastructure (still below); the homelab is now the main, actively-developed part.
📄 Overview for recruiters — what this is and why it exists, in plain language. 📄 Full setup & runbook — architecture diagram, step-by-step replication guide, and every bug hit along the way (with root causes, not just fixes).
- Cloud Provider: Oracle Cloud Infrastructure (OCI)
- Virtualization (Homelab): Proxmox VE, provisioned via the
bpg/proxmoxTerraform provider - Infrastructure as Code: Terraform
- Configuration Management: Ansible (+ Ansible Vault for secrets)
- Containerization: Docker & Docker Compose
- Container Orchestration: K3s (lightweight Kubernetes)
- Distributed Block Storage: Longhorn
- GitOps: ArgoCD
- Traffic Routing & SSL: Traefik / Nginx Proxy Manager
- Monitoring & Management: Portainer, Uptime Kuma, Watchtower (Docker layer) · kube-prometheus-stack (Kubernetes layer, GitOps-deployed)
Focus: Establish an indestructible baseline on the "Always Free" tier.
- Account Hardening: Enabled MFA for OCI login and set a budget alert (> €1.00).
- Base Instance: Provisioned an "Ampere A1" instance (ARM) with 4 Cores, 24 GB RAM, and Ubuntu.
- Cloud Network (VCN): Configured Ingress Rules (opened ports 80 & 443).
- Docker Setup: Installed Docker, Docker Compose, and Portainer for management (via the
docker_setup/deploy_composeAnsible roles).
Focus: Learn automation and resource management.
- Terraform Basics: Local setup and API keys configured via
.oci/config. - The Sandbox: Wrote scripts to automatically provision and destroy x86 test servers (
terraform apply&terraform destroy). - Version Control: Established a secure GitHub repository (including strict
.gitignoreto protect tfstate files and credentials).
Focus: Professional traffic routing and container updates.
- Reverse Proxy: Nginx Proxy Manager running as a Docker container, deployed via the
nginx_proxyAnsible role. - Domain Routing: Configured DNS records and Let's Encrypt for automatic SSL certificates.
- Watchtower: Implemented automated update management for running containers.
Focus: Deploy visible applications that utilize the server's resources.
- Monitoring: Deployed Uptime Kuma to monitor cloud services and the local homelab.
- Workspaces: Deployed Kasm in Docker to leverage the 24 GB RAM of the ARM instance for desktop streaming.
- Clean-Up: Ensured all manual and Terraform-generated x86 resources are completely destroyed.
Focus: Stand up a self-hosted, GitOps-managed Kubernetes platform on bare-metal Proxmox — "cattle, not pets" for the homelab.
- Proxmox IaC: Migrated to the
bpg/proxmoxTerraform provider; modularubuntu-vmmodule,for_each-driven, provisions a 3-node K3s cluster (1 control-plane + 2 workers) from a singlevmsmap. - Cloud-Init: Static IPs, injected SSH public key, and the QEMU guest agent enabled on every node.
- Dynamic Ansible Inventory: Terraform generates
ansible/inventories/production/hosts.iniwith[control_plane]/[workers]groups straight from noderole, ready for K3s bootstrapping playbooks. - K3s Cluster Bootstrap:
ansible/playbooks/install-k3s.ymlinstalls the K3s server on the control-plane, retrieves its node token, and joins both workers as agents. - Distributed Block Storage: Longhorn, backed by a Terraform-provisioned secondary disk (
scsi1, 50GB) attached to worker nodes only; formatted, and mounted at/var/lib/longhornviaansible/playbooks/setup-longhorn-nodes.yml. - GitOps Bootstrap: ArgoCD installed via
kubernetes/bootstrap/install-argocd.sh. - First GitOps App:
kube-prometheus-stackdeployed as an ArgoCDApplication(kubernetes/apps/kube-prometheus-stack.yaml), with Prometheus, Alertmanager, and Grafana PVCs all backed by Longhorn — proving persistent, replicated storage survives pod/node loss end to end. - K3s Ingress: K3s ships with Traefik by default, but it isn't yet configured/exposed for the new
monitoringnamespace or future apps.
Focus: close the gap between "it works" and "it's operable." Not yet implemented — a candidate list for what's next.
- App-of-Apps pattern:
kubernetes/apps/root-app.yamlnow watcheskubernetes/apps/(non-recursive) directly. Every future top-level.yamlunder that directory is auto-registered ongit push— no more one-time manualkubectl apply -fper app. - Remote Terraform State:
terraform.tfstateis still local-only — no locking, no team-safety, no recovery if the machine runningterraform applyis lost. - Longhorn Backups: No S3/NFS backup target is configured — losing more worker VMs than Longhorn's replica count would still mean real data loss. Blocked on NAS arrival.
- GitOps Secrets Management: Sealed Secrets controller deployed via ArgoCD; Longhorn UI's BasicAuth and Tandoor's DB credentials now live in git as
SealedSecretobjects, not created out-of-band. Immich's and BookStack's secrets are not yet migrated — see runbook. - CI Validation:
.github/workflows/validate-gitops.ymlruns yamllint,kubectl kustomizeon every overlay, and kubeconform schema validation against upstream Kubernetes' OpenAPI schemas on every push/PR tomain. Terraform/Ansible linting is still not covered. - Alerting: Alertmanager's default receiver now posts to an
ntfy-alertmanagerbridge in-cluster, which forwards to a private ntfy.sh topic with severity mapped to priority/tags. See runbook for the topic name. - K8s Ingress & TLS: Decide between K3s's built-in Traefik and the existing Docker-based Nginx Proxy Manager for exposing Grafana etc. externally, then wire up cert-manager or NPM accordingly.
remote-devops-lab/
├── .gitignore
├── README.md
│
├── terraform/
│ └── proxmox/
│ ├── main.tf
│ ├── providers.tf <-- bpg/proxmox provider
│ ├── variables.tf
│ ├── outputs.tf
│ ├── terraform.tfvars <-- gitignored, holds real credentials + the vms map
│ ├── terraform.tfvars.example
│ ├── inventory.tftpl <-- renders the Ansible inventory below
│ └── modules/
│ └── ubuntu-vm/ <-- one K3s node per for_each entry; optional Longhorn data disk
│ ├── main.tf
│ ├── variables.tf
│ ├── outputs.tf
│ └── providers.tf
│
├── ansible/
│ ├── ansible.cfg <-- Basic ansible settings
│ ├── inventories/
│ │ └── production/
│ │ ├── hosts.ini <-- AUTO-GENERATED by Terraform! Do not edit manually.
│ │ └── group_vars/
│ │ ├── all.yml
│ │ ├── docker_nodes.yml <-- Variables for all docker hosts
│ │ └── proxy_nodes.yml
│ │
│ ├── group_vars/
│ │ └── all/
│ │ └── vault.yml <-- Ansible Vault-encrypted secrets (sudo password, etc.)
│ │
│ ├── playbooks/
│ │ ├── site.yml <-- Master playbook (Docker/proxy nodes)
│ │ ├── docker.yml <-- Maps docker role to VMs
│ │ ├── proxy.yml <-- Maps proxy role to VMs
│ │ ├── install-k3s.yml <-- Bootstraps the K3s cluster itself (server + agent join)
│ │ └── setup-longhorn-nodes.yml <-- Preps K3s workers' secondary disk for Longhorn
│ │
│ └── roles/
│ ├── system_setup/ <-- Root/LVM partition growth, disk pre-flight checks
│ │ └── tasks/main.yml
│ ├── docker_setup/ <-- Installs docker engine
│ │ └── tasks/main.yml
│ ├── deploy_compose/ <-- Generic role to copy and run compose files
│ │ └── tasks/main.yml
│ └── nginx_proxy/
│ └── tasks/main.yml
│
├── kubernetes/ <-- GitOps layer for the K3s cluster
│ ├── bootstrap/
│ │ └── install-argocd.sh <-- One-time ArgoCD install onto K3s
│ └── apps/
│ └── kube-prometheus-stack.yaml <-- ArgoCD Application; Prometheus/Grafana PVCs on Longhorn
│
└── docker/
├── portainer/
│ └── docker-compose.yml
├── nginx-proxy-manager/
│ └── docker-compose.yml
└── monitoring/
├── docker-compose.yml
└── prometheus.yml <-- Docker-Compose-based Prometheus (separate from the K8s stack above)