vin.moe

how this site runs

This site is a static page served by nginx from a single-node Kubernetes cluster, which runs in a virtual machine on a server in my homelab. The only public address in front of it belongs to a RAM-only relay in Switzerland, which forwards traffic to the cluster over WireGuard. One repository defines all of it, from the host's firewall to the container image and the Deployment, in about 12,500 lines of Terraform that CI applies, with the state kept in HCP Terraform. The repository is the source of truth: changes are made there and applied from there, never against the cluster by hand.

The sections below start with the machine, follow a request in through the relay, follow one change from a commit to a running pod, and describe the platform it lands on.

AI wrote the HTML/CSS on this page. I do infrastructure, not frontend.

the machine

the cluster

the relay

ingress and dns

the image

serving

posts

deploying a change

  1. validation

    Every change starts on the integration branch. CI opens a merge request into production and arms auto-merge, and GitLab merges it once that pipeline passes. A merge request runs only the checks its paths call for. A change to a script runs the shared build-script test suite against a synthetic page, which covers the asset-hashing and cache-busting logic every site's build.sh has in common. The infrastructure plan skips the paths Terraform does not read, so editing HTML does not queue an apply on the node that also serves the page.

  2. build and digest

    buildctl pushes the :latest and commit-SHA tags against a registry-hosted layer cache, so unchanged stages, including the compiled nginx module, are pulled rather than rebuilt. The sha256 digest it returns, not the mutable tag, is recorded in a JSON file of image digests and committed by the job that built it. On a push race the retry resets to whatever landed first and records its digest again: two sites deploying at once write neighbouring lines of the same file, and a rebase would hit the same conflict on every attempt.

  3. rollout

    The rollout reads the digest back from git at the remote tip, so it deploys what was recorded rather than what a job variable held. It sets the image to that exact digest and waits on the rollout status.

  4. reconcile and mirror

    terraform apply needs only the digest-recording step, not the rollout. Both read the same committed digests, so the imperative rollout and the declarative apply converge on the same image reference whichever finishes first. The apply runs in its own CI pod: HCP Terraform only stores the state, and the job decrypts its inputs from a SOPS-encrypted file in the repository with CI's own key. Commits authored by CI push with the project's job token, which GitLab excludes from triggering a pipeline, and carry a [skip ci] marker as well. A final job pushes the site source to a public GitHub repository through a scoped token, committing only when something changed.

ci runners

observability

resilience

security