Skip to content
Go back

MetalLB for Bare-Metal LoadBalancer

By SumGuy 10 min read
MetalLB for Bare-Metal LoadBalancer
Contents

The Problem With No Cloud Load Balancer

You’ve got k3s running on some old enterprise boxes in your basement, and you deploy a service with type: LoadBalancer. Nothing happens. The service sits there in PENDING forever, waiting for a load balancer that will never come.

In AWS, GCP, Azure? That’s handled. A controller watches for your service and automatically provisions an ELB or similar. But on bare metal, your home lab, a co-located server, a small office, there’s no magic. No cloud provider. Just you, your hardware, and a networking problem.

MetalLB solves this. It watches your Kubernetes cluster, and when you create a LoadBalancer service, it actually assigns it a real IP address and makes the traffic work. No cloud, no special hardware. Just BGP or Layer 2 ARP and a lot of common sense.

What MetalLB Does

You’re hiring a traffic cop for your services. Instead of hiring an expensive city like AWS, MetalLB is the cop you train and keep yourself. It stands at the intersection and waves traffic toward the right road.

When you do:

apiVersion: v1
kind: Service
metadata:
name: my-api
spec:
type: LoadBalancer
selector:
app: my-api
ports:
- port: 80
targetPort: 8080

MetalLB claims an IP address from a pool you configure, assigns it to the service, and announces it to your network (either via Layer 2 broadcast or BGP route advertisements). Traffic destined for that IP gets distributed across the pods running my-api. No NodePort ugliness, no port forwarding scripts, no port number in the URL.

Your service has a real, usable IP address. Load-balanced traffic. And it’s all running on hardware you own.

The Two Modes: Layer 2 vs BGP

MetalLB has two operating modes, and picking the right one matters.

Layer 2: The Simple Answer

Layer 2 mode is straightforward. MetalLB picks an IP from a pool and has one node respond to ARP requests for it. Traffic arrives, gets load-balanced across the cluster.

How it works:

Pros:

Cons:

When to use it: Your home lab. A small office. Anywhere Layer 2 is available and you’ve got maybe 5 to 10 nodes max.

BGP: The Scalable Answer

BGP is older than the web. It’s the routing protocol that runs the internet. MetalLB can speak BGP, advertise your service IPs to your network, and let your upstream router decide which node gets the traffic.

How it works:

Pros:

Cons:

When to use it: Larger clusters (10+ nodes), multi-site setups, or if your router already speaks BGP. Overkill for a home lab with 3 nodes in a closet.

Installing MetalLB

Here’s the quick install. MetalLB runs as a controller deployment and a speaker daemonset.

Terminal window
kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/v0.16.0/config/manifests/metallb-native.yaml

Pin the version. If you apply the main branch manifest instead, you get container images tagged :main, which is whatever landed upstream this morning. v0.16.0 is current as of September 2026.

Wait for it to be ready:

Terminal window
kubectl wait --for=condition=ready pod \
-l component=controller \
-n metallb-system \
--timeout=300s

The manifest labels its pods app=metallb plus component=controller or component=speaker. It does not use the app.kubernetes.io/* prefix, so a selector built on that returns nothing and kubectl wait sits there until it times out.

One more thing if you’re on k3s: k3s ships its own LoadBalancer controller, ServiceLB (formerly Klipper), and it is on by default. Two controllers fighting over the same service is not a good time. Start your servers with --disable=servicelb before you install MetalLB.

Configuring Your IP Pool (Layer 2)

Create an IPAddressPool with the IPs you want MetalLB to use:

apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
name: default
namespace: metallb-system
spec:
addresses:
- 192.168.1.200-192.168.1.250

Now tell MetalLB to use Layer 2 for this pool:

apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
name: default
namespace: metallb-system
spec:
ipAddressPools:
- default

Apply both:

Terminal window
kubectl apply -f ippool.yaml -f l2advert.yaml

Now create a test service:

apiVersion: v1
kind: Service
metadata:
name: test-lb
spec:
type: LoadBalancer
selector:
app: test
ports:
- port: 80
targetPort: 8080

Check the status:

Terminal window
kubectl get svc test-lb

You should see an EXTERNAL-IP assigned from your pool (e.g., 192.168.1.200). Done.

Configuring BGP (Advanced)

If you’ve got a BGP-capable router (pfSense with the FRR package installed, VyOS, a real Juniper/Cisco device, or an EdgeRouter), here’s the idea.

First, create your IP pool:

apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
name: default
namespace: metallb-system
spec:
addresses:
- 192.168.1.200-192.168.1.250

Then configure MetalLB’s BGP settings:

apiVersion: metallb.io/v1beta2
kind: BGPPeer
metadata:
name: my-router
namespace: metallb-system
spec:
myASN: 64512
peerASN: 64512
peerAddress: 192.168.1.1
peerPort: 179
holdTime: 120s
keepaliveTime: 40s
---
apiVersion: metallb.io/v1beta1
kind: BGPAdvertisement
metadata:
name: default
namespace: metallb-system
spec:
ipAddressPools:
- default

Your router also needs BGP config. On pfSense, that’s roughly:

BGP AS Number: 64512
Router ID: 192.168.1.1 (or some ID on your network)

Then add a neighbor (MetalLB):

Peer AS: 64512
Peer IP: (any node in your cluster)

Note the API versions: BGPPeer moved to metallb.io/v1beta2 and the v1beta1 version of that resource is deprecated, so applying the older one gets you a warning from the API server. IPAddressPool, L2Advertisement and BGPAdvertisement are still v1beta1.

Once both sides are talking BGP, apply your MetalLB config and services get advertised. Your router learns the routes and directs traffic appropriately.

Reality check: This is complex if you’ve never touched BGP. There are timing issues, holdtime tuning, address families, and seven different ways to misconfigure it. For a home lab with 3 nodes? Layer 2. For a real cluster with 10+ nodes and a router you control? BGP buys you scale and network-level redundancy that Layer 2 mode cannot give you at all.

Common Gotchas

“My service has an IP but traffic doesn’t reach it.” Layer 2: Are you on the same network segment as the IP pool? ARP doesn’t cross routers. BGP: Is BGP actually up? Check the session from your router’s side first. MetalLB ships two BGP backends: the manifest above (metallb-native.yaml) uses MetalLB’s own Go implementation, so the speaker pod has no vtysh to poke at and you debug it from the speaker logs. If you installed metallb-frr.yaml or metallb-frr-k8s.yaml instead, the speaker pod runs FRR alongside it and vtysh -c "show bgp summary" works. Are address families correct?

“I’m getting flapping, the IP keeps moving between nodes.” Probably Layer 2 with unstable node ARP. Make sure your Layer 2 Advertisement specifies which nodes can own IPs (use nodeSelectors). Or switch to BGP.

“The IP is assigned but doesn’t show in my network.” BGP: Did you forget to advertise the route? Check kubectl logs -n metallb-system -l component=speaker. Layer 2: Is the node actually announcing? arp -an | grep 192.168.1.200 on a client machine should show the node’s MAC.

“I assigned a pool but nothing happened.” Did you apply an L2Advertisement or BGPAdvertisement pointing to the pool? The pool alone does nothing, it needs an advertisement to be useful.

When NOT to Use MetalLB

The Real Talk

MetalLB is one of those projects that sounds complicated until you deploy it. Layer 2 is two config files and about five minutes. BGP is real network engineering, but if you’re running it, you probably already know BGP or have a reason to learn it.

What you get out of it: load-balanced services on bare metal, with an actual LoadBalancer type that behaves the way it does in the cloud. That’s worth the five minutes of setup.

Your home lab or small business cluster finally stops looking like it’s running on a toy. Services get real VIPs, clients don’t care about NodePorts, and traffic distribution actually works. No cloud bill required.

Pick Layer 2. Deploy it in an afternoon. Thank yourself at 2 AM when you’re not hand-rolling port forwards anymore.

Common Questions

Do I need to disable k3s ServiceLB before installing MetalLB?

Yes. k3s enables ServiceLB (formerly Klipper) by default, and it claims LoadBalancer services using node host ports. Restart every k3s server with --disable=servicelb, then install MetalLB. Skip this and your services get node IPs from ServiceLB instead of addresses from your MetalLB pool.

Does MetalLB work on a single-node cluster?

Yes. MetalLB on one node still gives you real service IPs from your pool in Layer 2 mode, which is worth it just to stop juggling NodePort numbers. You lose the failover benefit, since there is no second node to take over the ARP ownership when the first one dies.

Can I run MetalLB and an Ingress controller together?

Yes, and that is the normal setup. You give the Ingress controller one LoadBalancer service, MetalLB assigns it a single IP from the pool, and the Ingress controller routes by hostname behind it. You only need more pool addresses when a service needs a raw TCP or UDP port instead of HTTP routing.

How many IP addresses should the pool have?

One per LoadBalancer service, plus room to grow. A home lab typically wants 10 to 20. Pick a range outside your DHCP scope, or your router will eventually hand one of those addresses to a laptop and you’ll spend an evening chasing a duplicate-IP conflict.


Share this post on:

Send a Webmention

Written about this post on your own site? Send a webmention and it'll show up above once verified.


Next Post
Longhorn vs OpenEBS for k3s Storage

Discussion

Powered by Garrul . Sign in with GitHub or Google, or post anonymously.

Related Posts