8000
Skip to content

Repository files navigation

🏠 Morty Labs β€” K3s Homelab

A lightweight K3s-based Kubernetes cluster, currently running version v1.34.4+k3s1 as of 25th February 2026, tailored for a personal homelab hosting Home Assistant, InfluxDB, Grafana, MQTT, UniFi Controller, Pi-hole, deCONZ, and more.

K3s cert-manager MetalLB ingress-nginx License: GPL-3.0 Stars CI

Raspberry Pi ARM64 Cloudflare


πŸ€” Why This Repo Exists

To better understand Kubernetes concepts, I wrote all deployment .yaml files myself from scratch, instead of relying on third-party Helm charts:

  • Fully orchestrated Kubernetes stack built for Raspberry Pi
  • Ideal for self-hosted services: Home Assistant, databases, dashboards, network management, and more
  • Modular and portable β€” spin up the same stack at home or at a remote site
  • Secure by default β€” TLS everywhere, Cloudflare WAF in front, fail2ban on the nodes

πŸ‘€ Who Is This For?

  • Raspberry Pi enthusiasts wanting to run a production-grade homelab or website
  • Home Assistant users who want persistent storage, TLS, and proper ingress
  • Anyone learning Kubernetes who wants real working manifests, not toy examples
  • DevOps engineers setting up a low-cost remote monitoring stack

🎯 What You'll End Up With

A fully working homelab cluster with:

  • 🏑 Home Assistant accessible at https://ha.yourdomain.com
  • πŸ“Š Grafana dashboards at https://grafana.yourdomain.com
  • πŸ”’ Automatic TLS certificates via Let's Encrypt
  • πŸ“‘ MQTT broker for all your IoT sensors
  • πŸ•³οΈ Network-wide ad blocking via Pi-hole
  • ☁️ Automatic DNS updates when your home IP changes
  • πŸ’Ύ All data persisted on NFS β€” survives pod restarts and reboots

πŸš€ Cluster Overview

  • Kubernetes Distribution: Rancher K3s
  • Load Balancer: MetalLB v0.15.3
  • Ingress/Reverse Proxy: ingress-nginx v1.12.0 (replacing the default Traefik)
  • TLS: cert-manager v1.19.4 + Let's Encrypt
  • DNS/CDN: Cloudflare (proxy + WAF + DDNS)
  • Nodes: Raspberry Pi 4 Model B β€” ARM64, 8GB RAM
  • Operating System: Raspberry Pi OS (Bookworm) 64-bit
  • Storage: Dedicated Raspberry Pi NFS server (SSD-based, 500GB)
  • Backups: Automatic backups to Google Drive and GitHub

Feel free to explore, reuse, or adapt this repo for your own Kubernetes learning journey!


πŸ“¦ Deployed Services

  • home-assistant β€” Core home automation hub
  • influxdb β€” Time-series metrics storage
  • grafana β€” Metrics dashboards
  • mqtt_broker β€” Mosquitto MQTT broker
  • mqtt2influx β€” MQTT β†’ InfluxDB bridge
  • govee2mqtt β€” Govee BLE lights β†’ MQTT
  • deconz β€” Zigbee gateway (ConBee II)
  • pihole β€” Network-wide ad/DNS blocking
  • unifi β€” Ubiquiti UniFi Controller
  • cloudflare-dns β€” Cloudflare DDNS CronJob
  • ingress β€” ingress-nginx + cert-manager ClusterIssuer
  • metallb β€” Bare-metal load balancer config
  • pv_nfs β€” NFS persistent volume provisioner
  • wordpress β€” WordPress + MariaDB
  • ingress-nginx β€” Cloudflare real IP forwarding config for ingress-nginx

πŸ›  Installation β€” k3s

Edit /boot/firmware/cmdline.txt (Bookworm) or /boot/cmdline.txt (Bullseye) and append to the single existing line:

cgroup_enable=cpuset cgroup_enable=memory cgroup_memory=1

Then install k3s with Traefik and the built-in service LB disabled:

curl -sfL https://get.k3s.io | sh -s - --disable=traefik --disable=servicelb --write-kubeconfig-mode 644

πŸ›  Installation β€” NFS Persistent Storage

Follow this tutorial to configure your Pi as an NFS server: https://pimylifeup.com/raspberry-pi-nfs/

Then deploy the NFS provisioner in k3s:

cd pv_nfs
kubectl apply -f class.yaml
kubectl apply -f rbac.yaml
kubectl apply -f deployment.yaml

πŸ›  Installation β€” NGINX Ingress

kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.15.1/deploy/static/provider/cloud/deploy.yaml

πŸ›  Installation β€” M 8000 etalLB Load Balancer

cd metallb
kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/v0.16.1/config/manifests/metallb-native.yaml
vi config.yaml  # edit and set the IP address range reserved on your DHCP server
kubectl apply -f config.yaml

πŸ›  Installation β€” HTTPS Ingress via cert-manager & Let's Encrypt

Below will install cert-manager v1.19.5, which is the latest version as of 18th June 2026.

cd ingress
kubectl create namespace cert-manager
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.19.5/cert-manager.yaml
vi letsencrypt.yaml  # update your email address
kubectl apply -f letsencrypt.yaml

Remember to open ports 80 and 443 on your router and forward traffic to the MetalLB ingress IP.


πŸ›  Installation β€” Applications

For each app, edit pv.yaml to set your NFS server IP and path, edit secrets.yaml to set credentials, then:

kubectl apply -f pv.yaml
kubectl apply -f secrets.yaml
kubectl apply -f deployment.yaml
kubectl apply -f svc.yaml
kubectl apply -f ingress.yaml  # if applicable

See each app's README.md for specific instructions.


πŸ” Secrets Management

Each app has a secrets.yaml file excluded from git via .gitignore. Never commit real credentials. See each app folder for the specific secrets required.


☁️ Cloudflare DNS

All services are exposed via Cloudflare-proxied subdomains. DNS records are kept in sync automatically via the cloudflare-dns CronJob which runs every 20 minutes.


⬆️ Upgrading k3s

curl -sfL https://get.k3s.io | sh -s - --disable=traefik --disable=servicelb --write-kubeconfig-mode 644
kubectl get nodes

Always check the k3s release notes before upgrading.


⚠️ Gotchas & Lessons Learned

TLS Renewal Fails Silently Behind Cloudflare Proxy

Domains proxied through Cloudflare (orange cloud) cannot use HTTP-01 challenges. Cloudflare intercepts the .well-known/acme-challenge request before it reaches the origin, returning a 526 error. cert-manager will silently rack up failed attempts (33 in one case) over weeks until the cert expires.

Fix: Use DNS-01 solver scoped to Cloudflare-proxied domains in the ClusterIssuer.

cert-manager Needs Its Own Copy of the Cloudflare Secret

cert-manager reads secrets from the cert-manager namespace, not default. The Cloudflare API token secret must exist in both namespaces:

  • default/ddns-secret-cloudfare-api-token β€” used by the DDNS CronJob
  • cert-manager/ddns-secret-cloudfare-api-token β€” used by cert-manager for DNS-01
kubectl create secret generic ddns-secret-cloudfare-api-token \
  --from-literal=CLOUDFARE_API_TOKEN=<token> \
  -n cert-manager

ClusterIssuer Solver Config (DNS-01 + HTTP-01)

solvers:
- dns01:
    cloudflare:
      apiTokenSecretRef:
        name: ddns-secret-cloudfare-api-token
        key: CLOUDFARE_API_TOKEN
  selector:
    dnsNames:
    - ha.mortylabs.com        # UK β€” Cloudflare proxied
    - sa.mortylabs.com        # SA β€” Cloudflare proxied
    - unifisa.mortylabs.com   # SA Unifi β€” Cloudflare proxied
- http01:
    ingress:
      class: nginx            # fallback for non-proxied domains

Monitoring Cert Expiry

kubectl get certificates -A                          # check READY status
kubectl get orders -A                                # check for invalid orders
kubectl describe certificate <name> -n default       # see failure reason
kubectl logs -n cert-manager deployment/cert-manager # check controller logs

Built with β˜• and mild obsession by Andrew Morty 🏴󠁧󠁒󠁳󠁣󠁴󠁿

About

Andrew Morty's production-grade K3s homelab on Raspberry Pi β€” Home Assistant, InfluxDB, Grafana, MQTT, Pi-hole, deCONZ, UniFi, Cloudflare DDNS and more

Topics

Resources

Stars

26 stars

Watchers

3 watching

Forks

Releases

Packages

Used by

Contributors

Languages

0