This site is driven by two keys, like the machine it came from. Reading: j and k scroll, d and u move half a page, gg goes to the top, G to the bottom, H and L switch windows, ? opens the help. Space is the Neovim leader and handles content: Space then h home, r research, p projects, g gear, a about, / tags, or Space then a digit to jump to that window. Home is the tmux prefix and handles windows: Home then c opens a terminal, Home then & closes a window, Home then space goes to the next one. While focus is on the keyboard, h j k l move between keys and Enter opens one.

ResearchR AboutA Gear — G for gear — keyboard, terminal, editor, homelab.G
    ProjectsP Tags — / as in vim: search./
    ×
    Menu

    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:

    1. Push to master on the site repo.
    2. GitHub Actions builds the image and pushes it to GHCR as ghcr.io/arisros/arisjirat-site:latest.
    3. ArgoCD Image Updater watches that tag, spots the new digest, and writes the digest back into the homelab-platform repo — straight into overlays/production/kustomization.yaml.
    4. 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

    LayerTool
    OrchestrationKubernetes via k3d
    GitOpsArgoCD + ArgoCD Image Updater
    ManifestsKustomize (base + overlays), Helm for infrastructure
    MonitoringPrometheus + Grafana + Alertmanager
    LoggingLoki + Grafana Alloy
    TracingTempo (OpenTelemetry)
    SecretsHashiCorp Vault
    Reverse proxyCaddy, on the VPS and in the homelab
    VPNWireGuard tunnel
    DNS / CDNCloudflare (mixed proxy mode)
    CI/CDGitHub 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.

    id en
    rss gh in