From Static HTML to Ghost: Migrating My Portfolio on Kubernetes
How I replaced a static HTML portfolio baked into a Kubernetes ConfigMap with a full Ghost CMS, deployed via GitOps with zero downtime.
For the past several months, my personal site at steven.naviauxlab.com was a single static HTML file baked into a Kubernetes ConfigMap and served by nginx. Every time I wanted to change a word, I had to edit HTML, commit to Git, push, and wait for Flux to reconcile. It worked, but it was friction. And friction kills publishing.
This week I replaced the whole thing with Ghost, a proper CMS with a visual editor, running on the same homelab Kubernetes cluster. Here's how.
Why Ghost?
I evaluated a few options: Hugo, Jekyll, WordPress, Ghost. I wanted something that:
- Has a built-in visual editor (no more raw HTML)
- Runs self-hosted on Kubernetes
- Uses SQLite (no separate database pod on a resource-constrained single node)
- Has a clean, modern design out of the box
- Is lightweight enough for a 16GB single-node cluster
Ghost checked every box. The Alpine image is small, SQLite means one PVC covers everything, and the admin UI is excellent.
The Architecture
Ghost runs as a single-replica Deployment with a 5Gi Longhorn PVC for content (SQLite database, uploaded images, and themes). Traffic flows through Cloudflare Tunnel to Traefik to the Ghost pod.
Key specs:
- Image:
ghost:6.19.2-alpine - Database: SQLite at
/var/lib/ghost/content/data/ghost.db - Resources: 50m/128Mi requests, 500m/384Mi limits
- Security:
runAsNonRoot,runAsUser: 1000, dropped NET_RAW capability
The Redirect Loop
The first deploy hit an immediate ERR_TOO_MANY_REDIRECTS. Ghost was configured with url: https://steven.naviauxlab.com, but Traefik receives plain HTTP from Cloudflare Tunnel internally. Ghost saw HTTP, redirected to HTTPS, Cloudflare sent it back as HTTP, infinite loop.
The fix: add the https-scheme Traefik middleware to the ingress, which sets X-Forwarded-Proto: https. Ghost sees the header and stops redirecting. This is the same pattern used by every other app in the cluster that sits behind Cloudflare Tunnel.
What Got Deleted
The old portfolio was seven files: namespace, kustomization, deployment (nginx + ConfigMap), service, ingress, and network policies. All gone. Replaced by the Ghost manifests following the exact same patterns: same namespace convention, same dual-host ingress, same default-deny network policies.
What's Next
Now I can publish from my phone, add images without editing YAML, and focus on content instead of plumbing. The friction is gone. Let's see if I actually write more.