Why Self-Host a Static Site on Kubernetes?
If you’re already running a homelab or production Kubernetes cluster, deploying a static site there makes sense. You get full control over your infrastructure, TLS via cert-manager, and it’s a great learning exercise.
In this post, I’ll walk through how I deploy this very site — built with Hugo and the PaperMod theme — to my K8s cluster.
The Stack
| Component | Tool |
|---|---|
| Static Site Gen | Hugo |
| Theme | PaperMod |
| Container | Nginx Alpine |
| Orchestration | Kubernetes |
| Deployment | ArgoCD / Helm |
| TLS | cert-manager |
| DNS | Cloudflare |
Building the Container Image
The multi-stage Dockerfile keeps things lean:
FROM hugomods/hugo:exts as builder
WORKDIR /src
COPY . .
RUN hugo --minify
FROM nginx:alpine
COPY --from=builder /src/public /usr/share/nginx/html
EXPOSE 80
Build and push:
docker build -t registry.example.com/mikefrantum-dev:latest .
docker push registry.example.com/mikefrantum-dev:latest
Kubernetes Manifests
Here’s a basic deployment:
apiVersion: apps/v1
kind: Deployment
metadata:
name: mikefrantum-dev
namespace: web
spec:
replicas: 2
selector:
matchLabels:
app: mikefrantum-dev
template:
metadata:
labels:
app: mikefrantum-dev
spec:
containers:
- name: hugo
image: registry.example.com/mikefrantum-dev:latest
ports:
- containerPort: 80
Adding an Ingress with TLS
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: mikefrantum-dev
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
tls:
- hosts:
- mikefrantum.dev
secretName: mikefrantum-dev-tls
rules:
- host: mikefrantum.dev
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: mikefrantum-dev
port:
number: 80
GitOps with ArgoCD
Once your manifests are in a Git repo, ArgoCD can watch for changes and auto-sync:
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: mikefrantum-dev
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/mikefrantum/mikefrantum-dev-deploy
targetRevision: HEAD
path: k8s
destination:
server: https://kubernetes.default.svc
namespace: web
syncPolicy:
automated:
prune: true
selfHeal: true
What’s Next
- Add monitoring with Prometheus + Grafana
- Set up a CI pipeline with GitHub Actions to auto-build on push
- Explore caching strategies with Cloudflare
This is a test post for styling purposes.