MetalLB and Kubernetes Bare-Metal Networking: LoadBalancers Without a Cloud

Giving your on-prem cluster the one feature the cloud takes for granted

The first thing that goes wrong when you build a Kubernetes cluster on your own hardware is the most anticlimactic possible failure. You deploy something, set its Service type to LoadBalancer the way every tutorial told you to, and then… nothing. kubectl get svc shows EXTERNAL-IP stuck on <pending>, forever, with the quiet patience of a thing that is never going to happen.

This is not a bug. On a cloud provider, type: LoadBalancer is a request that the cloud’s controller fulfils by provisioning an actual load balancer and handing you a public IP. On bare metal there is no cloud controller listening, so the request just sits there. MetalLB is the missing piece: it implements that controller for your own network, so a homelab or on-prem cluster can finally do the thing the cloud does for free.

Advertisement

MetalLB watches for Services of type LoadBalancer, allocates each one an IP from a pool you define, and then makes the rest of your network actually route traffic to that IP. That second half is the clever bit, and it comes in two flavours.

In Layer 2 mode, one node becomes the “leader” for each service IP and answers ARP requests for it. To your router and switches, that IP looks like it lives on that node’s NIC. Traffic for the IP arrives at the leader, and kube-proxy takes it from there. It’s dead simple — no router configuration needed — but all traffic for a given IP funnels through a single node, and failover means waiting for ARP caches to update.

In BGP mode, MetalLB speaks BGP to your router and advertises service IPs as routes, with true per-session load balancing across nodes. It’s the grown-up option, but it assumes you have a router that speaks BGP and you’re comfortable configuring it. For most homelabs, Layer 2 is the honest, correct choice.

Install the manifests, then give MetalLB an address pool. Pick a range on your LAN subnet that your DHCP server will never hand out — this is the part people botch, and a clash produces gloriously confusing intermittent failures.

apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
  name: lan-pool
  namespace: metallb-system
spec:
  addresses:
    - 192.168.1.240-192.168.1.250
---
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
  name: lan-advert
  namespace: metallb-system
spec:
  ipAddressPools:
    - lan-pool

That’s it. The IPAddressPool says “these eleven addresses are mine to give out,” and the L2Advertisement says “announce them with ARP.” Now deploy any Service and watch it get a real, pingable IP:

$ kubectl get svc nginx
NAME    TYPE           CLUSTER-IP     EXTERNAL-IP      PORT(S)        AGE
nginx   LoadBalancer   10.96.14.7     192.168.1.241    80:31385/TCP   12s

That EXTERNAL-IP is now reachable from anywhere on your LAN. Point a browser at it, add a DNS record, put it behind your ingress — it just works, finally.

A few things will bite you, so let me save you the evenings I lost.

  • The address pool must be on your LAN subnet and outside DHCP. MetalLB doesn’t create a subnet; it borrows free addresses from your existing one. Overlap with DHCP and you’ll get duplicate-IP chaos that comes and goes with the mood of your router.
  • Layer 2 is not load balancing. Despite the name, a single node handles all traffic for each IP. You get high availability (another node takes over on failure) but not horizontal throughput. For a home cluster that’s almost always fine.
  • Some consumer switches dislike rapid ARP changes. Failover in L2 mode can take a few seconds while caches expire, and the occasional cheap switch with aggressive ARP security will make this worse.
  • Annotate to pin or share IPs. You can request a specific address with metallb.io/loadBalancerIPs, or let multiple services share one IP with metallb.io/allow-shared-ip when ports don’t collide.

A common confusion: do you need MetalLB and an ingress controller? Usually yes, and they’re complementary. The typical pattern is one MetalLB-assigned IP feeding an ingress controller (Traefik, nginx, whatever you like), which then routes by hostname to all your services. MetalLB gives you the single stable entry IP; ingress does the layer-7 fan-out behind it. You’re not choosing between them — you’re stacking them.

If you run Kubernetes on hardware you own, MetalLB isn’t optional so much as inevitable. It’s the component that turns a cluster that technically works into one that behaves like every tutorial assumes, where type: LoadBalancer produces an IP instead of an awkward silence. Layer 2 mode takes about ten minutes to set up and asks almost nothing of your network. BGP mode is there when you outgrow that, but most people never will. For the home-cluster crowd, it’s a near-mandatory install that I reach for on every bare-metal cluster I build — and one of the rare bits of infrastructure I set up once and genuinely never think about again.

Advertisement

Related Content

Advertisement
Smarc
Written by Smarc

Founder and editor of vo.rs. A lifelong tinkerer who self-hosts far more than is sensible, hardens Linux boxes for fun, and prods the latest AI tools to see what they can really do. The how-to guides here are the notes Smarc wishes had existed the first time round.