Homelab Kubernetes Series: 1. Intro · 2. Installation · 3. Configuration · 4. Networking · 5. Storage · 6. Workloads · 7. ArgoCD · 8. GitOps
Recap
By the last post I had certificates and secrets sorted out. None of that matters if traffic can’t actually reach a pod, so this post covers the path a request takes from a browser on my LAN to a container running in the cluster.
MetalLB: giving the cluster real LAN addresses
Bare-metal Kubernetes has no cloud provider to hand out LoadBalancer IPs for you, which is the gap MetalLB fills. It’s a MicroK8s addon here, so there’s no manifest of my own to show, but the effect is that type: LoadBalancer Services get a real, dedicated IP out of a pool I control instead of sitting in <pending> forever.
Every application in the cluster shares one pattern: it sits behind Traefik, and Traefik gets exactly one of those addresses. The one exception is DNS. Pi-hole needs to be the DNS resolver for the network, on a fixed, memorable IP, answering on the actual DNS port, and routing that through an HTTP reverse proxy doesn’t make sense. So it gets its own dedicated LoadBalancer IP straight from the pool:
| |
Every device on my network points its DNS at that one address.
Traefik: one ingress, host-based routing
Traefik is installed by hand via Helm rather than a MicroK8s addon:
| |
The values file is intentionally small — a dashboard IngressRoute and one forward-looking setting: the Kubernetes Gateway API provider is turned on.
| |
That Gateway API setting isn’t actually doing anything yet. Every workload in the cluster still routes through a regular Kubernetes Ingress, which is the older, more common way to expose a Service. Gateway API is the newer replacement for Ingress, and I turned Traefik’s support for it on so it’s ready if I ever want to move a workload over. For now it’s just sitting there switched on and unused.
The real pattern is one dedicated subdomain per app: app1.joeyaxtell.com, pihole.joeyaxtell.com, grafana.joeyaxtell.com, argocd.joeyaxtell.com, and so on, each with an Ingress that references the same TLS ClusterIssuer from the last post. Traefik terminates TLS, matches the Host() rule, and forwards to the matching in-cluster Service.
DNS: Pi-hole as the front door for hostnames
With Pi-hole holding a fixed LAN address and handling DNS for the whole network, every one of those *.joeyaxtell.com hostnames just needs a local DNS entry pointing back at Traefik’s address. That’s what makes “internal-only, real TLS cert, friendly hostname” work together: Pi-hole resolves the name, Traefik terminates the certificate and routes by host header, and from a browser’s perspective it looks exactly like a normal public site.
Pi-hole is also the clearest example of the Sealed Secrets pattern from the last post in practice — its upstream resolvers and web admin password are stored as an encrypted SealedSecret and pulled in with envFrom.secretRef, instead of sitting in the deployment manifest as plain text.
What’s deliberately not here
No service mesh, no Tailscale or WireGuard overlay. Everything described here is LAN-only, reachable because Pi-hole resolves the hostname and MetalLB/Traefik own the routing. A few workloads in my media namespace have their own networking requirements beyond this, but I’m leaving those out since they’re app-specific rather than cluster networking concerns.
Up next
Traffic can reach a pod now. Next up: what happens to the data once it gets there — the two storage tiers I ended up running side by side, and why.
◀ Previous: 3. Configuration | Next ▶: 5. Storage