Infrastructure / Blog
Astro static site → Docker image → Kubernetes → Cloudflare Tunnel → whenkat.com.
Everything is automated from a git push. No manual build steps, no SSH-ing into servers.
The GitHub Actions workflow watches blog/** and only triggers on changes there. A change to an unrelated manifest won't rebuild the blog image.
whenkat.com traffic to the blog service via an HTTPRoute.
Create a Markdown file in blog/src/content/blog/ with this frontmatter:
---
title: 'My Post Title'
description: 'One sentence shown in link previews.'
pubDate: 'Jan 15 2026'
series: 'AI Server Build' # groups posts on the index page
seriesOrder: 3 # position within the series
---
Your content here in Markdown...
The series and seriesOrder fields control how the post appears on the landing page. Posts in the same series are grouped together and sorted by seriesOrder. Omit both fields for a standalone post — it lands in "Uncategorized".
git add the file, commit, and git push. GitHub Actions builds the image, pushes to ghcr.io, and ArgoCD rolls it out within a couple of minutes.
The Dockerfile is a two-stage build. Node is used only at build time and is not present in the final image:
# Stage 1 — build (Node 22 Alpine)
npm ci && npm run build # outputs to /app/dist
# Stage 2 — runtime (nginx Alpine)
# copies /app/dist → /usr/share/nginx/html
# ~25MB total image, no Node, no npm
The image is private on ghcr.io. The cluster uses a docker-registry secret named ghcr-pull-secret in the blog namespace to pull it. Recreate it with:
make blog-pull-secret # reads secrets/ghcr-pull-token.txt
All managed by ArgoCD from gitops/manifests/blog/:
Namespace: blog
Deployment: blog (2 replicas, ghcr.io/pycaster/proxmox-k8s-blog:latest)
Service: blog (ClusterIP, port 80)
HTTPRoute: blog (whenkat.com + www.whenkat.com → blog:80)
The HTTPRoute lives in gitops/manifests/gateway/blog-route.yaml and references the homelab Gateway in the default namespace. NGF applies it and begins routing traffic as soon as the blog pods are ready.