--- title: "Shipping this site: GitOps from a homelab to the public internet" date: 2026-06-15 summary: "How this portfolio is built and served — Astro to a container image, a self-hosted Gitea registry, ArgoCD, and a Cloudflare Tunnel — with security as acceptance criteria, not polish." tags: ["gitops", "astro", "homelab", "security"] --- This site is a static Astro build, but how it gets to you is the interesting part. It's served from my homelab Kubernetes cluster over a Cloudflare Tunnel, deployed the same way I'd ship anything else: as an immutable image, pinned by digest, reconciled by GitOps. ## The pipeline 1. The site is built and baked into a hardened `nginx-unprivileged` image. 2. The image is pushed to a **self-hosted public Gitea registry** — deliberately separate from the private instance that holds my infrastructure code. 3. The image digest is pinned in a private `home-ops` repo. 4. **ArgoCD** reconciles that repo onto the cluster. 5. A **Cloudflare Tunnel** exposes exactly one service — this site — outbound-only. No open ports. No server runtime. No registry credential on the cluster, because the public package is anonymous-pull and the image holds nothing secret. ## Security as acceptance criteria The interesting constraint was treating security as a checklist to *pass*, not a vibe: ```text [x] Static output — no server runtime to attack [x] Strict CSP, no unsafe-inline / unsafe-eval [x] Self-hosted fonts — zero third-party requests [x] No secrets in the client bundle (verified by build-time grep) [x] Outbound-only tunnel, single hostname, no catch-all ``` ## Why bother Because the site *is* the argument. A platform engineer's portfolio should demonstrate the discipline it's advertising — and "it's a static page" is no excuse to skip the rigour. The deployment story is part of the work.