--- 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, Argo CD, and a Cloudflare Tunnel - with security as acceptance criteria, not polish." tags: ["gitops", "astro", "homelab", "security"] hero: "/blog/shipping-this-site.webp" heroAlt: "A glowing data container travels a luminous rail from a small server rack toward a softly glowing globe of Earth, wrapped by a reconcile loop." --- This site is a static Astro build, and that's the least interesting thing about it. What matters is how it reaches you: served from my homelab Kubernetes cluster over a Cloudflare Tunnel, shipped the way I'd ship anything I actually cared about - an immutable image, pinned by digest, reconciled by GitOps. No special case for "it's just a website." ## The pipeline The build is baked into a hardened `nginx-unprivileged` image and pushed to a **self-hosted public Gitea registry**, deliberately a separate instance from the private one holding my infrastructure code, so the public artifact and the private estate never share a trust boundary. The image digest is then pinned in a private `home-ops` repo, **Argo CD** reconciles that repo onto the cluster, and a **Cloudflare Tunnel** exposes exactly one service, this site, outbound-only. Follow the chain and notice what's missing. No open ports: the tunnel dials out. No server runtime: the output is static files behind nginx. No registry credential on the cluster: the public package is anonymous-pull, and the image holds nothing secret to protect. Every link in the pipeline is either immutable, declarative, or absent. ## Security as acceptance criteria The discipline that made it work was treating security as a checklist to *pass*, not a vibe to gesture at. The site didn't ship until every box was ticked: ```text [x] Static output - no server runtime to attack [x] CSP: script-src 'self', no inline or eval JavaScript (style-src keeps 'unsafe-inline' for Shiki) [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 ``` A checklist sounds bureaucratic until you notice what it changes: each item is a claim you can verify, and a failing item blocks the ship. "Pretty secure" isn't a state you can test for. `grep` finding zero secrets in the bundle is. ## The site is the argument The site *is* the argument. A platform engineer's portfolio should demonstrate the discipline it advertises, and "it's a static page" is no excuse to skip the rigour - it's the cheapest possible place to practise it. If the pipeline behind a brochure site is immutable, verified, and zero-trust, that's not overkill. That's the standard, rehearsed where the stakes are low so it holds where they aren't.