August 20, 2025·4 min read
This blog is both a playground and a living documentation platform for my experiments.
It’s running directly on a Raspberry Pi 5 sitting on my desk. I use Gatsby.js as the static site generator, Docker for packaging, K3s (a lightweight Kubernetes distribution) for orchestration, and Helm to make deployments repeatable. Everything is exposed securely via Cloudflare Tunnel.
This post is a deep dive into how I put this together — not just the commands, but the reasoning behind each step, the pitfalls I hit, and the lessons learned along the way.
1. Why Raspberry Pi and Kubernetes?
Before diving into commands, let’s talk about why I even did this.
- Raspberry Pi 5: It’s cheap ($60), ARM64-based, and powerful enough to run real workloads. With 8GB RAM, it can host Kubernetes pods and serve real traffic.
- K3s: Normal Kubernetes is overkill for a Pi. K3s is trimmed down, lightweight, and optimized for edge devices and IoT. Perfect for homelabs.
- Gatsby.js: Static site generators make blogs blazing fast, secure, and easy to deploy as containers.
- Docker + Helm: Docker lets me package my site so it runs the same everywhere. Helm makes redeployment a one-liner instead of managing multiple
kubectlconfigs.
So this stack is about learning and also proving that a $60 Pi can host a “production-grade” site.
2. Setting up K3s on the Pi
I originally tried MicroK8s, but ran into snapd version issues on Raspberry Pi OS. K3s was dead simple:
curl -sfL https://get.k3s.io | sh -Check the node:
sudo k3s kubectl get nodes -o wideThis gives you the single-node cluster ready to schedule workloads.
Key notes:
- K3s bundles containerd by default (no need for Docker runtime).
- NodePorts and LoadBalancers just work.
- Systemd integration makes K3s restart automatically on reboot.
3. Building the Blog with Gatsby
Start with Gatsby’s starter template:
npm init gatsby
cd blog
npm install
npm run developThis gives you hot reloading and a skeleton site. I then customized it:
- Added a terminal-inspired theme (CRT effects, scanlines, boot logs).
- Created Markdown-based blog posts inside
/content/posts. - Added tags to frontmatter so I could filter posts later.
Once the site worked locally, I ran:
gatsby buildThis outputs static HTML into /public — ready to be served.
4. Dockerizing Gatsby Output
The goal: run Gatsby’s static HTML in Nginx inside a container.
Here’s the Dockerfile:
FROM nginx:alpine
COPY public /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.confThe nginx.conf ensures single-page app routing works correctly (important for 404 pages and client-side navigation):
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
location / {
try_files $uri /index.html;
}
}Then build for ARM64:
docker buildx build --platform linux/arm64 -t 192.168.1.66:32358/gatsby-blog:latest . --pushVerify:
docker buildx imagetools inspect 192.168.1.66:32358/gatsby-blog:latestThis shows manifest lists (good for ARM64 compatibility).
5. Local Container Registry on the Pi
I didn’t want to push to Docker Hub, so I use the Pi as its own registry (via containerd + NodePort).
After pushing, confirm the image is there:
sudo ctr -n k8s.io images ls | grep gatsby-blogIf it shows up, the cluster can pull it.
Pro tip: Add insecure-registries to your Docker config if you’re not using TLS on the Pi registry:
{
"insecure-registries": ["192.168.1.66:32358"]
}6. Deploying with Helm
Instead of manually writing kubectl apply -f, I use Helm. My chart:
# values.yaml
image:
repository: 192.168.1.66:32358/gatsby-blog
tag: latest
pullPolicy: Always
service:
type: NodePort
port: 80
targetPort: 80
nodePort: 32360
ingress:
enabled: true
className: "nginx"
hosts:
- host: blog.chrishouse.io
paths:
- path: /
pathType: Prefix
tls:
- secretName: blog-tls
hosts:
- blog.chrishouse.ioDeploy:
helm upgrade --install gatsby ./helm -n blog --create-namespace --set image.repository=192.168.1.66:32358/gatsby-blog --set image.tag=latestKubernetes pulls from the Pi registry, starts a pod, and exposes it at NodePort 32360.
Check it:
sudo kubectl -n blog get pods -o wide
sudo kubectl -n blog get svc gatsby-gatsby-blog7. Exposing with Cloudflare Tunnel
I didn’t want to port forward. Enter Cloudflare Tunnel:
tunnel: 4eab82fd-9172-4c0f-aaeb-237c72452dbe
credentials-file: /home/chris/.cloudflared/4eab82fd-9172-4c0f-aaeb-237c72452dbe.json
ingress:
- hostname: blog.chrishouse.io
service: http://localhost:32360
- service: http_status:404Install and run:
cloudflared tunnel create blog
cloudflared tunnel route dns blog blog.chrishouse.io
sudo cloudflared service install
sudo systemctl start cloudflaredNow requests to https://blog.chrishouse.io are routed securely through Cloudflare.
Benefit: No exposed home IP, no router config, free TLS.
8. Automating the Workflow
I don’t want to manually run 10 commands. So in package.json I added:
"scripts": {
"build": "gatsby build",
"docker:build": "docker buildx build --platform linux/arm64 -t 192.168.1.66:32358/gatsby-blog:latest . --push",
"helm:deploy": "helm upgrade --install gatsby ./helm -n blog --create-namespace --set image.repository=192.168.1.66:32358/gatsby-blog --set image.tag=latest",
"deploy": "npm run build && npm run docker:build && npm run helm:deploy"
}Now it’s literally one command:
npm run deploy9. Lessons Learned Along the Way
- Caching issues: Sometimes pods don’t pull the latest image.
kubectl rollout restart deploy gatsby-gatsby-blogfixes it. - Resource limits: The Pi can handle ~10 pods, but keep an eye on memory.
- Cloudflare TLS: Don’t fight self-signed certs. Terminate TLS at Cloudflare edge.
- Helm DRY: Extract common charts into a shared repo (
my_helm). - Logging: Container logs are visible in
kubectl logsand shipped to Elastic.
10. Future Plans
- Add CI/CD via GitHub Actions to trigger
npm run deploy. - Add Grafana dashboards on the same Pi (already running Prometheus).
- Expand posts and make
/blog/:slugthe default for navigation.
Final Thoughts
With Gatsby, Docker, Helm, and Cloudflare Tunnel, I now have a full production-like pipeline running on a Raspberry Pi.
This setup is overkill for a personal blog — but that’s the point. It’s a playground to learn Kubernetes, CI/CD, and security practices in a safe, low-cost way.
If a Pi can do it, so can your enterprise cluster.
Enjoyed this post? Give it a clap!
Comments