Setting Up MetalLB on K3s
estimated read time: 5 minutes
What is MetalLB?
MetalLB offers a Layer 2 or BGP load balancer for Kubernetes.

Why Use It?
Kubernetes does not provide an implementation of network load balancers (Services of type LoadBalancer) for bare-metal clusters. The load balancer implementations that ship with Kubernetes are glue code that calls out to various IaaS platforms like GCP, AWS, and Azure.
If you’re not running on a supported IaaS platform, LoadBalancer services will remain in a “pending” state indefinitely. This leaves part of the Kubernetes stack out of reach for those setting up clusters on bare metal rather than in the cloud.
For home-labbers like myself—where the freedom to experiment, cost considerations, and the ability to get under the covers are factors—this presents a problem. Thankfully, MetalLB provides a solution.
To be clear, you don’t need a load balancer for most web-based homelab projects. Normally, Ingress or NodePorts will suffice. If you don’t have use cases requiring:
- The same port reused across multiple services
- IP addresses instead of DNS entries
then either of the above will be fine. However, there are scenarios where a load balancer is beneficial—for example, DNS servers or NTP hosts where you want high availability and redundancy across multiple servers on the same port.
MetalLB acts as an alternative to running a physical load balancer, NodePort, or Ingress rule. It emulates a service type normally found only in cloud environments. This allows you to focus on your Kubernetes stack rather than retrofitting Helm charts for non-cloud configurations.
Setting Up Layer 2 MetalLB in Your Homelab
There are several ways to install MetalLB. I found using the script from their website the easiest approach.
Installing MetalLB
We’ll use the most basic setup: a Layer 2 listener.
What is a Layer 2 Load Balancer?
In the OSI model, Layer 2 (the Data Link Layer) is where data links between physical devices are established and terminated. In your homelab, this represents the endpoint you’ll interact with.
Because we’re using Layer 2 announcements, the IP range allocated to MetalLB must be reserved exclusively for its use. In a typical home network, your router also acts as your DHCP server. MetalLB sends out an ARP announcement to claim its IP range. If you’re not careful, this could overlap with the range your DHCP server uses to allocate addresses, leading to IP conflicts.
For small networks, the probability of collision is low—roughly n/254. For larger networks, your DHCP server may have allocated many addresses that could overlap with MetalLB’s range.
To avoid conflicts, ensure you have a sufficiently large IP range for MetalLB and that your router’s DHCP range doesn’t overlap. Since MetalLB doesn’t reserve these IPs with your router, it’s recommended to use a different subnet. For example, if your DHCP server allocates 192.168.0.2–192.168.0.254, consider using 192.168.1.0/24 (netmask 255.255.255.0) for MetalLB, giving it an entirely unique range.
You’ll also need to update your router’s routing table. There are too many router vendors and configurations to provide detailed guidance here—please consult your vendor’s documentation.
Registering the IP Pool
Below is an example configuration for a Layer 2 LoadBalancer. Ensure the pool size is large enough for current needs and leaves room for future expansion.
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
name: first-pool
namespace: metallb-system
spec:
addresses:
- 192.168.101.20-192.168.101.28
autoAssign: true
avoidBuggyIPs: true
Setting Up Layer 2 Announcements
As discussed, the Layer 2 load balancer uses ARP announcements rather than requesting an IP from your router. This announcement is broadcast to your network and picked up by your router, which updates its routing table to direct traffic for MetalLB’s IP range to the appropriate node.
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
name: l2advertisement-1
namespace: metallb-system
spec:
ipAddressPools:
- first-pool
Limitations of Layer 2 Load Balancing
Layer 2 load balancers have some drawbacks:
- Single-node bottleneck: All traffic for a service IP goes to one leader node, limiting ingress bandwidth to that node’s capacity.
- Slow failover: Failover depends on clients updating their neighbour caches based on gratuitous Layer 2 packets from the new leader. Some clients may not handle these packets correctly or promptly, causing service disruption.
Layer 2 may not be suitable for high-traffic or mission-critical applications. Additionally, unless you’ve followed the advice above to ensure unique IP ranges, you risk conflicts with your DHCP server.
Using MetalLB in Your Service
Below is an example service that presents itself on the local network using IP 192.168.101.80, accepting traffic on port 21 and forwarding it to the target port on the foo-dns pod in the foo namespace.
This is one of my personal use cases: setting up a DNS server available on the local network.
apiVersion: v1
kind: Service
metadata:
name: foo-service
namespace: foo
annotations:
metallb.universe.tf/allow-shared-ip: foo-frontend
spec:
selector:
name: foo-dns
ports:
- port: 21
targetPort: 21
protocol: TCP
type: LoadBalancer
loadBalancerIP: "192.168.101.80"
Pi Cluster Series
- Part 1 — Introduction: Pi Cluster
- Part 2 — Setting Up the Cluster
- Part 3 — Persistent Storage with Longhorn
- Part 4 — Offsite Backups with AWS S3
- Part 5 — Load Balancing with MetalLB ← you are here
- Part 6 — Ingress and TLS with Traefik v3
- Part 7 — Edge Protection with Cloudflare