How to Set Up MetalLB for Load Balancing on a Bare-Metal Kubernetes Cluster
Cloud-hosted Kubernetes clusters get load balancers automatically provisioned by AWS, GCP, or Azure the moment you request a LoadBalancer service. A homelab cluster running on your own hardware — like the Raspberry Pi K3s cluster covered in an earlier guide — has no such cloud provider to call, which is exactly the gap MetalLB for bare-metal Kubernetes fills.
Why Bare-Metal Clusters Need MetalLB
Without a cloud provider’s load balancer integration, a Kubernetes LoadBalancer service request on a self-hosted cluster gets stuck in a permanently “Pending” state, since nothing exists to actually assign it an external IP address. MetalLB solves this by implementing load balancer functionality directly within your own network, assigning real IP addresses from a range you control to services that request one.
How MetalLB Works
MetalLB operates in one of two modes:
- Layer 2 mode – the simplest option, where MetalLB responds to ARP requests for the assigned IP, making one cluster node responsible for that IP at a time (with automatic failover if that node goes down)
- BGP mode – integrates with actual network routing hardware supporting BGP, distributing traffic across multiple nodes simultaneously; more complex to set up but offers genuine load distribution rather than single-node responsibility
Most homelab setups start with Layer 2 mode due to its simplicity and lack of dependency on specialized routing hardware.
Installing MetalLB
For a K3s cluster, install MetalLB using its official manifest:
kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/main/config/manifests/metallb-native.yaml
Configuring an IP Address Pool
Define a range of IP addresses from your home network that MetalLB can assign to services — chosen carefully to avoid conflicting with your router’s DHCP range:
yaml
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
name: homelab-pool
namespace: metallb-system
spec:
addresses:
- 192.168.1.200-192.168.1.220
Apply this configuration:
kubectl apply -f ipaddresspool.yaml
Enabling Layer 2 Advertisement
Create an accompanying L2Advertisement resource so MetalLB actually announces the assigned IPs on your local network:
yaml
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
name: homelab-l2
namespace: metallb-system
spec:
ipAddressPools:
- homelab-pool
Testing with a Sample Service
Deploy a simple test service with type LoadBalancer:
yaml
apiVersion: v1
kind: Service
metadata:
name: test-service
spec:
type: LoadBalancer
selector:
app: test-app
ports:
- port: 80
targetPort: 80
Check that MetalLB assigns an external IP from your configured pool:
kubectl get svc test-service
Adding an Ingress Controller
While MetalLB assigns IPs to individual services, most homelab clusters run many services and don’t want a separate IP for each one. An Ingress controller (commonly NGINX Ingress or Traefik) sits behind a single MetalLB-assigned IP and routes incoming requests to the correct internal service based on hostname or path, similar in concept to the reverse proxy setup covered in the Nginx Proxy Manager guide, but built specifically for Kubernetes.
Installing NGINX Ingress Controller
kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/main/deploy/static/provider/baremetal/deploy.yaml
Once installed, the Ingress controller’s own service receives a single MetalLB-assigned IP, and individual Ingress resources within your cluster define hostname-based routing rules to different backend services — one external IP handling traffic for many internal applications.
Final Thoughts
Setting up MetalLB for bare-metal Kubernetes closes one of the most noticeable gaps between running Kubernetes on cloud infrastructure versus your own homelab hardware. Combined with an Ingress controller, it gives your Raspberry Pi or Proxmox-based K3s cluster the same practical load balancing and routing capabilities you’d expect from a cloud-hosted cluster, entirely self-contained on your own network.