Homelab: The GitOps That Deploys This Site
Kubernetes on k3d at home, exposed through a VPS and a WireGuard tunnel, with ArgoCD pulling changes from Git. Including the page you're reading.
This homelab runs a full observability stack on Kubernetes (k3d), with public exposure brokered through a VPS over WireGuard. The page you’re reading is deployed by the same machinery.
Architecture
The request path is built so the homelab is never directly connected to the public internet. Cloudflare terminates TLS at the edge, a VPS acts as reverse proxy, and a WireGuard tunnel carries traffic the rest of the way in:
Internet → Cloudflare → VPS (Caddy) → WireGuard tunnel → homelab Caddy → k3d NodePort → Pod
Worth underlining: no inbound port is opened at home. The tunnel is built from the inside out. The only thing with a public address is the VPS, and the only thing reachable from the VPS is what was deliberately pointed at it.
All traffic goes through host-level Caddy proxies rather than DNAT. That isn’t an aesthetic choice — Docker bridge isolation makes DNAT behave differently than you’d expect, and Caddy on the host is far easier to reason about when something is wrong at two in the morning.
Not kubectl apply
Nothing here is deployed by hand. The homelab-platform repo is the source of truth and ArgoCD pulls from it. App manifests are Kustomize (base + overlays/production), infrastructure is Helm values, and argocd/ holds the Application CRDs.
The deploy chain for this site runs like this:
- Push to
masteron the site repo. - GitHub Actions builds the image and pushes it to GHCR as
ghcr.io/arisros/arisjirat-site:latest. - ArgoCD Image Updater watches that tag, spots the new digest, and writes the digest back into the
homelab-platformrepo — straight intooverlays/production/kustomization.yaml. - ArgoCD sees the new commit in its GitOps repo and syncs the cluster.
argocd-image-updater.argoproj.io/image-list: site=ghcr.io/arisros/arisjirat-site:latest
argocd-image-updater.argoproj.io/site.update-strategy: digest
argocd-image-updater.argoproj.io/write-back-method: git
argocd-image-updater.argoproj.io/write-back-target: kustomization
The strategy is digest, not latest, and that’s the part that matters. A moving latest tag means Kubernetes can be running an image that differs from what Git says, with nothing able to tell you which image is actually live. With the digest pinned back into Git, what the cluster contains is always readable from a commit. Rollback becomes git revert rather than trying to remember which tag was in use.
syncPolicy has selfHeal and prune on. Which means any change I make directly against the cluster gets reverted by ArgoCD — infuriating exactly once, and reassuring every time after that.
Stack
| Layer | Tool |
|---|---|
| Orchestration | Kubernetes via k3d |
| GitOps | ArgoCD + ArgoCD Image Updater |
| Manifests | Kustomize (base + overlays), Helm for infrastructure |
| Monitoring | Prometheus + Grafana + Alertmanager |
| Logging | Loki + Grafana Alloy |
| Tracing | Tempo (OpenTelemetry) |
| Secrets | HashiCorp Vault |
| Reverse proxy | Caddy, on the VPS and in the homelab |
| VPN | WireGuard tunnel |
| DNS / CDN | Cloudflare (mixed proxy mode) |
| CI/CD | GitHub Actions → GHCR → GitOps |
Monitoring
Every service ships telemetry to Grafana, which is the single pane of glass across all three observability signals:
- Dashboards — 35+ panels covering cluster, application and infrastructure health.
- Alerting — routed through Alertmanager to Telegram.
- SLOs — burn-rate policies decide when to wake someone up, rather than raw thresholds.
The observability plane itself stays private, reachable only over WireGuard. Grafana has no public address, and doesn’t need one.
What runs here
Besides this site: the pothole detection demo, a WhatsApp notification bot, a scheduled attendance runner, a few internal applications, and the Vault instance holding their credentials. All of them deploy through the same path — one directory under apps/, one Application CRD, and no manual steps.