Kubernetes Networking in the Homelab: Traefik, MetalLB, and Pi-hole

How traffic gets from my LAN to a pod: a MetalLB address pool, Traefik as the ingress layer, and Pi-hole handling DNS for every joeyaxtell.com subdomain on the network.

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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
apiVersion: v1
kind: Service
metadata:
  name: pihole-dns
spec:
  type: LoadBalancer
  loadBalancerIP: 192.168.0.241
  ports:
    - name: dns-udp
      port: 53
      protocol: UDP
    - name: dns-tcp
      port: 53
      protocol: TCP

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:

1
helm install traefik traefik/traefik -f traefik_values.yaml --wait

The values file is intentionally small — a dashboard IngressRoute and one forward-looking setting: the Kubernetes Gateway API provider is turned on.

1
2
3
4
5
6
7
8
providers:
  kubernetesGateway:
    enabled: true
gateway:
  listeners:
    web:
      namespacePolicy:
        from: All

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

comments powered by Disqus
Built with Hugo
Theme Stack designed by Jimmy