Network Setup: MetalLB + Traefik
The Problem
Kubernetes on bare metal lacks cloud LoadBalancer support. Without it:
- No external IPs for services
- No easy way to expose apps
- Manual port forwarding hell
MetalLB to the Rescue
MetalLB provides LoadBalancer implementation for bare metal.
Installation
kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/v0.13.12/config/manifests/metallb-native.yaml
Configuration
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
name: default-pool
namespace: metallb-system
spec:
addresses:
- 192.168.1.200-192.168.1.220
---
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
name: default
namespace: metallb-system
spec:
ipAddressPools:
- default-pool
Now services with type: LoadBalancer get real IPs from this pool.
How It Works
- Service created with
type: LoadBalancer - MetalLB assigns IP from pool
- L2 mode announces IP via ARP
- Local network routes traffic to assigned IP
- MetalLB forwards to service endpoints
Example Service
apiVersion: v1
kind: Service
metadata:
name: my-app
spec:
type: LoadBalancer
ports:
- port: 80
targetPort: 8080
selector:
app: my-app
Result:
kubectl get svc my-app
NAME TYPE EXTERNAL-IP PORT(S)
my-app LoadBalancer 192.168.1.200 80:30123/TCP
Accessible at http://192.168.1.200 from any device on the network.
Traefik Ingress Controller
LoadBalancers use one IP per service. For HTTP(S) apps, use Ingress:
- Share single IP across multiple services
- Virtual host routing
- Automatic TLS
- Middleware (auth, rate limiting, etc.)
Installation
apiVersion: v1
kind: Namespace
metadata:
name: traefik
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: traefik
namespace: traefik
spec:
replicas: 1
selector:
matchLabels:
app: traefik
template:
metadata:
labels:
app: traefik
spec:
serviceAccountName: traefik
containers:
- name: traefik
image: traefik:v2.10
ports:
- name: web
containerPort: 80
- name: websecure
containerPort: 443
- name: admin
containerPort: 8080
args:
- --api.insecure=true
- --providers.kubernetesingress=true
- --entrypoints.web.address=:80
- --entrypoints.websecure.address=:443
---
apiVersion: v1
kind: Service
metadata:
name: traefik
namespace: traefik
spec:
type: LoadBalancer
ports:
- name: web
port: 80
targetPort: 80
- name: websecure
port: 443
targetPort: 443
selector:
app: traefik
Traefik gets 192.168.1.200 from MetalLB.
Using Ingress
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: blog
namespace: default
spec:
rules:
- host: lab.tomarc.dev
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: blog
port:
number: 80
- host: console.tomarc.dev
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: console
port:
number: 3000
Both services accessible via same IP, different hostnames.
DNS Configuration
Point domains to LoadBalancer IP:
lab.tomarc.dev A 192.168.1.200
console.tomarc.dev A 192.168.1.200
*.lab.tomarc.dev A 192.168.1.200
Traefik routes based on Host header.
Network Architecture
Internet
│
▼
Home Router (192.168.1.1)
│
├─ Mac (192.168.1.x) - Dev machine
├─ iDRAC (192.168.1.190) - Management
├─ R430 Node (192.168.1.100) - Kubernetes
│
└─ MetalLB Pool (192.168.1.200-220)
│
├─ Traefik (192.168.1.200) - HTTP/HTTPS
├─ Console API (192.168.1.201) - Management
└─ Future services (192.168.1.202+)
Pod Networking (Flannel)
Pods get IPs from internal network:
cluster:
network:
cni:
name: flannel
dnsDomain: cluster.local
podSubnets:
- 10.244.0.0/16
serviceSubnets:
- 10.96.0.0/12
- Pods:
10.244.0.0/16 - Services:
10.96.0.0/12 - Nodes:
192.168.1.0/24 - LoadBalancers:
192.168.1.200-220
Flannel handles pod-to-pod communication across nodes (single node for now, but ready to scale).
Service Types Comparison
ClusterIP (default)
- Internal only
- Other pods can reach it
- Not accessible from outside
NodePort
- Exposes on node IP:port
- Port range: 30000-32767
- Accessible but awkward ports
LoadBalancer
- External IP from MetalLB
- Standard ports (80, 443, etc.)
- Best for direct access
Ingress
- HTTP(S) only
- Shares LoadBalancer IP
- Best for web apps
Exposing to Internet
Option 1: Port Forward at Router
Router Port 80 → 192.168.1.200:80 (Traefik)
Router Port 443 → 192.168.1.200:443 (Traefik)
All HTTP traffic to your public IP goes to Traefik, which routes internally.
Option 2: Cloudflare Tunnel
Zero port forwarding:
cloudflared tunnel create homelab
cloudflared tunnel route dns homelab lab.tomarc.dev
cloudflared tunnel run homelab
Secure tunnel from Cloudflare to your Traefik. No firewall changes needed.
Option 3: Tailscale
VPN mesh network:
# On cluster
kubectl apply -f tailscale-operator.yaml
# Access from anywhere
curl http://console.tom-lab.ts.net
Secure, no public exposure.
Firewall Rules
Keep it simple:
- Allow local network (192.168.1.0/24) to all services
- Allow specific external access via port forward or tunnel
- Block everything else
Kubernetes NetworkPolicies for internal segmentation (future).
Performance
MetalLB L2 mode:
- Near-zero overhead
- Uses standard ARP
- No BGP complexity
- Perfect for single location
Traefik:
- ~5ms routing overhead
- Handles thousands of req/s
- Efficient reverse proxy
Monitoring
Check LoadBalancer assignment:
kubectl get svc --all-namespaces | grep LoadBalancer
Traefik dashboard:
kubectl port-forward -n traefik svc/traefik 8080:8080
# Visit http://localhost:8080/dashboard/
MetalLB logs:
kubectl logs -n metallb-system -l app=metallb
Troubleshooting
Service stuck in Pending
- MetalLB not installed
- No available IPs in pool
- Check MetalLB speaker pods
Ingress not routing
- DNS not pointing to LoadBalancer IP
- Traefik not running
- Incorrect Ingress host/path
Can’t reach from external
- Firewall blocking
- Router not forwarding
- Cloudflare tunnel down
This network setup provides a solid foundation. Simple, scalable, and follows Kubernetes best practices.
Previous: Architecture Overview