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