Files
bztmon-site/src/content/blog/shipping-this-site.md
T
2026-08-18 01:12:54 +10:00

2.8 KiB

title, date, summary, tags, hero, heroAlt
title date summary tags hero heroAlt
Shipping this site: GitOps from a homelab to the public internet 2026-06-15 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.
gitops
astro
homelab
security
/blog/shipping-this-site.webp 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:

[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.