GitOps homelab running on Proxmox (Intel NUC i7, 64 GB RAM). Built as a learning platform for modern cloud-native practices and as a public portfolio of platform engineering work.
- GitOps — all cluster state lives in this repository; no manual changes to infrastructure
- Zero trust — network-level and identity/auth enforced throughout
- Observability — metrics, logs, and alerting across the full stack
- Documentation-first — decisions and architecture captured alongside the code
See infrastructure diagram — auto-generated from infrastructure/proxmox/*.tf on every push.
See cluster diagram — auto-generated from infrastructure/ manifests and Helm charts on every push.
| Layer | Tool |
|---|---|
| Kubernetes | k3s |
| GitOps | ArgoCD |
| CNI | Cilium |
| Remote access | Tailscale |
| Ingress | Gateway API (via Cilium) |
| Public ingress | Cloudflare Tunnel |
| Certificates | cert-manager + Let's Encrypt (DNS-01 via Cloudflare) |
| DNS | Split-horizon: external-dns (UniFi, internal LAN) + external-dns (Cloudflare, public internet) |
| Observability | Grafana + Prometheus + Loki |
| Storage | Longhorn |
| Secrets | Sealed Secrets |
| CI | GitHub Actions |
| Identity / SSO | Authentik |
| AI inference | llama.cpp server (Gemma 4 26B A4B) |
| AI frontend | Open WebUI |
| Web search | SearXNG |
clusters/
home/
infrastructure/ # ArgoCD Applications for core infrastructure
apps/ # ArgoCD Applications for user-facing apps
infrastructure/ # Helm values, Kubernetes manifests, and Terraform per component
proxmox/ # Terraform — Proxmox VMs + Ansible inventory
unifi/ # Terraform — UniFi DNS records and DHCP reservations
tailscale-acl/ # Tailscale ACL (HuJSON) — applied to tailnet via CI
argocd/
cilium/ # Bootstrap reference values (helm upgrade, not ArgoCD)
cilium-config/ # ArgoCD-managed Cilium CRD instances (LoadBalancer IP pool)
cert-manager/
gateway/ # networking namespace, wildcard Certificate, Gateway resource
external-dns-internal/ # external-dns → UniFi (internal LAN DNS)
external-dns-external/ # external-dns → Cloudflare (public internet DNS)
sealed-secrets/
tailscale/
monitoring/
longhorn/
authentik/
ai/ # llama.cpp server + SearXNG + Open WebUI
apps/ # ArgoCD Applications for self-hosted apps
docs/ # Architecture notes and ADRs
.github/
workflows/ # GitHub Actions CI pipelines
This repo uses the ArgoCD App of Apps pattern. A root Application in clusters/home/ points to the infrastructure/ and apps/ directories. ArgoCD reconciles all child Applications automatically.
- Provision VMs on Proxmox
- Install k3s with Cilium as CNI
- Bootstrap ArgoCD — all subsequent installs are managed via ArgoCD
- Sealed Secrets controller + Tailscale operator
- Gateway API CRDs (bootstrap) + Cilium upgrade + cert-manager + Gateway + split-horizon external-dns (UniFi internal + Cloudflare external) + Cloudflare Tunnel
- Longhorn (persistent storage — required before Observability and app deployments)
- Observability stack (kube-prometheus-stack + Loki)
- Authentik (SSO for all services)
- Self-hosted apps
- Proxmox host reachable at
192.168.2.x - SSH key in
~/.ssh/with access to all nodes - Terraform variables available (see
infrastructure/proxmox/terraform.tfvars.exampleandinfrastructure/unifi/terraform.tfvars.example)
Proxmox VMs and UniFi DNS/DHCP records are managed in separate Terraform root modules. In CI, both run automatically on push. To run locally:
cd infrastructure/proxmox
terraform init
terraform apply
cd ../unifi
terraform init
terraform apply -parallelism=1 # required: provider has a concurrency limitationAnsible uses a dynamic inventory sourced from Terraform state — no manual IP management needed.
cd ansible
ansible-playbook playbooks/k3s.ymlNodes will register but show NotReady — that's expected until Cilium is installed.
Longhorn requires open-iscsi (block storage) and nfs-common on every cluster node. Run this before ArgoCD deploys Longhorn:
cd ansible
ansible-playbook playbooks/longhorn-prereqs.ymlk3s places its kubeconfig on the control plane at /etc/rancher/k3s/k3s.yaml. Copy it locally:
mkdir -p ~/.kube
ssh admin@192.168.2.10 "sudo cp /etc/rancher/k3s/k3s.yaml /tmp/k3s.yaml && sudo chmod 644 /tmp/k3s.yaml"
scp admin@192.168.2.10:/tmp/k3s.yaml ~/.kube/config
sed -i 's/127.0.0.1/192.168.2.10/g' ~/.kube/config
chmod 600 ~/.kube/configVerify: kubectl get nodes — all nodes should show NotReady.
Cilium must be installed directly — it cannot be managed by ArgoCD because the cluster network must exist before ArgoCD can run.
Install the Cilium CLI if not present:
CILIUM_CLI_VERSION=$(curl -s https://raw.githubusercontent.com/cilium/cilium-cli/main/stable.txt)
curl -sL "https://github.com/cilium/cilium-cli/releases/download/${CILIUM_CLI_VERSION}/cilium-linux-amd64.tar.gz" -o /tmp/cilium.tar.gz
sudo tar xzvf /tmp/cilium.tar.gz -C /usr/local/binInstall Cilium into the cluster:
cilium install --version 1.19.1 \
--set cluster.name=homelab \
--set ipam.mode=kubernetes \
--set kubeProxyReplacement=true \
--set k8sServiceHost=192.168.2.10 \
--set k8sServicePort=6443Note:
k8sServiceHostandk8sServicePortare required. Without them, Cilium's init container tries to reach the API server via the service IP (10.43.0.1), which doesn't exist yet — classic chicken-and-egg withkubeProxyReplacement=true.
Wait for Cilium to become healthy:
cilium status --waitVerify all nodes are Ready:
kubectl get nodesInstall ArgoCD into the cluster directly via Helm — this is the last manual install step:
helm repo add argo https://argoproj.github.io/argo-helm
helm repo update
helm install argocd argo/argo-cd \
--namespace argocd \
--create-namespace \
--version 7.7.0Wait for ArgoCD to be ready:
kubectl wait --for=condition=available deployment/argocd-server -n argocd --timeout=120sApply the root Application — this is the single manual kubectl apply that hands control to ArgoCD:
kubectl apply -f bootstrap/root-application.yamlArgoCD will discover clusters/home/infrastructure/, create all child Applications, and begin reconciling the cluster to match Git. From this point on, all changes go through Git.
Gateway API CRDs and the Cilium upgrade are bootstrap steps — applied directly like Cilium and ArgoCD.
Install Gateway API CRDs (v1.2.1 — matches Cilium 1.19.x):
kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.2.1/standard-install.yamlUpgrade Cilium to enable the Gateway API controller:
helm repo add cilium https://helm.cilium.io/
helm upgrade cilium cilium/cilium -n kube-system -f infrastructure/cilium/values.yamlArgoCD then manages the CiliumLoadBalancerIPPool, networking namespace, wildcard Certificate, and Gateway resources via the cilium-config and gateway Applications.