How This Site Works
This site is self-hosted on my own hardware. The content is Markdown, the generator is Hugo, the hosting is a Kubernetes cluster in my house. I write a post, commit, push, and it's live. This post is about why and how.
Why Self-Host
The practical argument for self-hosting has never been better. Consumer internet connections are fast enough to serve a website – mine is a symmetric gigabit fibre line. An old desktop draws less power than a light bulb and has more compute than you'd ever need for serving web traffic. The cost is negligible compared to what you'd pay for equivalent hosting, and you get complete control over the stack.
Complete control is the real draw. No build minute quotas, no bandwidth caps, no feature gates, no terms of service changes. The CI builds run on my hardware. The container registry is on my network. The only thing leaving my network is the HTTPS traffic and the ACME challenges for Let's Encrypt certificate renewal.
It's also just an interesting challenge. I do infrastructure professionally, and running my own is how I learn and experiment with things I wouldn't try at work. This site is a small part of a larger homelab – the same cluster runs home automation, internal tools, and a handful of other sites. Adding another static site to an existing cluster costs almost nothing.
Why Hugo
Static sites are remarkably efficient. There's no application server, no database, no server-side rendering – just files served by nginx. A static site on modest hardware can handle more traffic than I'll ever see, and there's very little that can go wrong at runtime. The site either serves files or it doesn't.
The harder problem is authoring. Writing raw HTML is tedious and error-prone. Keeping headers, footers, navigation, and styling consistent across pages by hand is a nightmare. Every static site generator exists to solve this – you write content in Markdown, define templates for the layout, and the generator produces the HTML.
I've tried several over the years. Jekyll was the first one I used seriously. There were others whose names I've forgotten. Hugo is the one that stuck. It's fast – this site builds in about 100 milliseconds – and it handles the things I care about well: Markdown content, template inheritance, taxonomies for tags and categories, and a theme ecosystem that means I don't have to build a design from scratch.
The theme is hugo-clarity, which handles layout, typography, dark mode, search, and responsive design. I override a few pieces – the sidebar links, the footer, a shortcode for embedding GPS tracks – but the theme does most of the work.
How It All Fits Together
The machine is an old desktop running K3s – a lightweight Kubernetes distribution that's small and mostly self-contained while being fully featured. It runs everything in the homelab: this site, home automation, internal services, and whatever I'm experimenting with at the time.
The site's source lives in a git repository on a self-hosted Gitea instance. Gitea has built-in CI/CD called Gitea Actions, which uses GitHub Actions workflow syntax. Every push triggers a build: checkout the repo, run Hugo to generate the static files, package them into a Docker image – just nginx:alpine with the HTML copied in – and push the image to an internal container registry.
If the push is to main, a second job deploys the image to Kubernetes. It pins the image to the exact commit SHA, applies the manifests, and Kubernetes rolls out the new version. Health checks confirm nginx is responding before the old pod is terminated.
The Kubernetes side is straightforward: a Deployment running one nginx container, a Service for internal routing, and an Ingress that exposes it to the internet with TLS from cert-manager and Let's Encrypt. Three manifests, about 80 lines total.
The end result is that the deployment pipeline is invisible. I write in Markdown, preview locally with hugo server, commit and push when it's ready, and the site updates itself. That's exactly how I want it – the infrastructure should get out of the way of the content.