
August 21, 2026
10 min read
Table of Contents
By Kokil Thapa | Last reviewed: August 2026
Running Kubernetes on bare metal with MetalLB removes the dependency on proprietary cloud load balancers while giving you full control over network topology and infrastructure costs. Unlike managed environments where a LoadBalancer Service automatically provisions external IPs, bare-metal clusters leave these services in a perpetual "Pending" state until you provide an IP address management solution. MetalLB fills this gap by implementing a network load balancer that integrates directly with your existing router and switch infrastructure.
Why Run Kubernetes on Bare Metal with MetalLB Instead of Cloud?
Before diving into configuration, it is worth understanding why teams choose this path when managed options exist. For developers building scalable tech solutions for startups or internal platforms in Nepal, the economics often dictate self-hosted infrastructure. A single managed Kubernetes cluster with load balancers can cost NPR 15,000–30,000 monthly; bare metal amortizes hardware over years and eliminates per-service egress fees.
Beyond cost, bare metal offers deterministic performance. There is no hypervisor tax, no noisy neighbors, and no opaque networking layers between your pods and the physical NIC. When I have deployed legal-tech portals requiring strict data residency within Nepal, bare metal was frequently the only compliant option because the hardware physically resides in a local data center rather than an overseas availability zone.
MetalLB specifically solves the "LoadBalancer pending" problem. Without it, you are forced to use NodePort services (exposing high ports like 30080) or Ingress controllers bound to specific nodes with manual DNS. MetalLB assigns real IPs from your local subnet to Services, making them indistinguishable from cloud-provisioned load balancers to the rest of your application stack.
How Do You Install MetalLB on a Bare-Metal Kubernetes Cluster?
Installation assumes you already have a working Kubernetes cluster (v1.29+ recommended for 2026 stability). Whether you built it with kubeadm, kubespray, or Talos, MetalLB installs identically via standard manifests or Helm. I prefer Helm for production because it simplifies upgrades and value overrides.
Add the MetalLB Helm Repository
helm repo add metallb https://metallb.github.io/metallb
helm repo update Create the Namespace and Install
kubectl create namespace metallb-system
helm install metallb metallb/metallb \
--namespace metallb-system \
--version 0.14.9 \
--set speaker.frr.enabled=true The --set speaker.frr.enabled=true flag is critical if you plan to use BGP mode. FRRouting (FRR) is the underlying BGP daemon that MetalLB uses to announce routes to your physical router. Even if you start with Layer 2, enabling FRR now avoids a disruptive reinstall later when you need to scale.
Verify Component Health
kubectl get pods -n metallb-system
# Expected output:
# NAME READY STATUS RESTARTS AGE
# controller-7d8f9b6c4-xk2lm 1/1 Running 0 2m
# speaker-a8f2k 1/1 Running 0 2m
# speaker-b3j9n 1/1 Running 0 2m
# speaker-c7m4p 1/1 Running 0 2m You must see one controller pod and one speaker pod per node. The controller handles IP allocation and CRD validation; the speakers handle the actual protocol announcements (ARP/NDP for L2, BGP for routed). If speakers are stuck in CrashLoopBackOff, check kubectl logs -n metallb-system <speaker-pod> — common causes include missing kernel modules (ip_vs) or conflicting CNI configurations.
How Do You Configure IPAddressPool and L2Advertisement?
MetalLB v0.14+ uses Custom Resources instead of ConfigMaps. This is a breaking change from older tutorials still circulating online. You define two resources: an IPAddressPool specifying which IPs are available, and an advertisement resource telling MetalLB how to announce them.
Define the IP Address Pool
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
name: production-pool
namespace: metallb-system
spec:
addresses:
- 192.168.10.100-192.168.10.150
- 192.168.10.200/30
avoidBuggyIPs: true
autoAssign: true The addresses field accepts CIDR notation, explicit ranges, or individual IPs. Setting avoidBuggyIPs: true excludes network and broadcast addresses automatically — a safeguard I always enable after debugging a client project where .0 and .255 were accidentally assigned to services, causing intermittent connectivity failures.
Configure Layer 2 Advertisement
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
name: l2-advert
namespace: metallb-system
spec:
ipAddressPools:
- production-pool
interfaces:
- eth0 Specifying interfaces is optional but strongly recommended on multi-homed servers. Without it, MetalLB may attempt ARP responses on management or storage networks, causing confusion and potential security exposure. On Ubuntu 22.04/24.04 servers I typically manage, the primary interface is usually eth0 or ens18; verify with ip addr before applying.
Apply both resources:
kubectl apply -f ipaddresspool.yaml
kubectl apply -f l2advertisement.yaml Test with a Sample Service
apiVersion: v1
kind: Service
metadata:
name: test-lb
spec:
type: LoadBalancer
selector:
app: nginx
ports:
- port: 80
targetPort: 80 Within seconds, kubectl get svc test-lb should show an EXTERNAL-IP from your pool. If it remains Pending, check kubectl describe svc test-lb for events and kubectl logs -n metallb-system controller-* for allocation errors.
When Should You Use BGP Mode Instead of Layer 2?
This is the most consequential architectural decision in any MetalLB deployment. Layer 2 mode is simpler but has a fundamental limitation: all traffic for a given service IP flows through a single node at any moment. The speaker on that node responds to ARP requests; other nodes remain idle for that IP. Failover works (another node takes over if the leader fails), but you never get horizontal scaling at the network layer.
BGP mode announces the same IP from every node simultaneously. Your upstream router performs ECMP (Equal-Cost Multi-Path) hashing, distributing packets across all healthy nodes. This gives you true load balancing at the network edge, not just at the kube-proxy level within a single node.
| Criteria | Layer 2 Mode | BGP Mode |
|---|---|---|
| Router Requirement | None (standard L2 switch) | BGP-capable router (MikroTik, VyOS, Cisco, Juniper) |
| Traffic Distribution | Single active node per IP | ECMP across all nodes |
| Failover Speed | ~10-30 seconds (ARP reannouncement) | <1 second (BGP withdrawal) |
| Configuration Complexity | Low (two YAML files) | Moderate (router BGP config + peer definitions) |
| Scalability Ceiling | Limited by single node NIC/CPU | Scales linearly with node count |
| Best For | Dev/staging, small prod, non-BGP networks | Production workloads, high-throughput APIs |
In practice, I default to BGP for any production system handling more than ~500 concurrent connections. The additional router configuration pays for itself immediately during rolling updates and node maintenance, where BGP's sub-second convergence prevents user-visible errors that Layer 2's ARP-based failover cannot avoid.
Configuring BGP Peers
apiVersion: metallb.io/v1beta2
kind: BGPPeer
metadata:
name: core-router
namespace: metallb-system
spec:
myASN: 64500
peerASN: 64501
peerAddress: 192.168.10.1
holdTime: 90s
passwordSecret:
name: bgp-secret
key: password Pair this with a BGPAdvertisement resource referencing your pool. Ensure your router is configured to accept BGP sessions from each node's IP and to redistribute those routes into your LAN routing table. Test with vtysh on the router or kubectl exec -n metallb-system <speaker-pod> -- vtysh -c "show bgp summary" to verify session establishment.
What Are Common Production Pitfalls with MetalLB on Bare Metal?
After deploying MetalLB across multiple client environments, several failure modes recur consistently. Addressing these proactively saves hours of debugging.
- IP conflicts with DHCP: Never overlap your MetalLB pool with DHCP ranges. Reserve a dedicated static block in your router/DHCP server and document it. I have seen entire clusters destabilize when a laptop received a MetalLB-assigned IP via DHCP.
- CNI incompatibility: Calico in VXLAN mode can interfere with MetalLB's ARP responses. If using Calico, switch to IPIP or native BGP peering. Cilium and Flannel generally work without issues.
- Missing kernel modules: Speaker pods require
ip_vs,ip_vs_rr, andnf_conntrack. On minimal Ubuntu installs, load them persistently via/etc/modules-load.d/metallb.conf. - Firewall blocking speaker traffic: UFW or iptables must allow UDP 7946 (memberlist gossip) and TCP 179 (BGP) between nodes. Blocking these causes split-brain IP assignments or failed BGP sessions.
- Stale ARP caches: After changing pools or modes, flush ARP caches on clients and intermediate switches. Linux clients:
ip neigh flush all. Windows:arp -d *.
How Does MetalLB Integrate with Ingress Controllers on Bare Metal?
MetalLB does not replace your Ingress controller; it complements it. In a typical bare-metal setup, MetalLB assigns a single external IP to your Ingress controller's Service (type: LoadBalancer). All HTTP/HTTPS traffic enters through that IP, and the Ingress controller routes to backend services based on host/path rules.
This pattern means you usually need only one or two IPs from your MetalLB pool for ingress, regardless of how many applications you run. Reserve additional IPs only for services that must bypass HTTP routing (TCP/UDP databases, mail servers, custom protocols).
For teams familiar with Laravel or PHP application deployment patterns discussed in Laravel development guides, think of MetalLB as the equivalent of assigning a public IP to your Nginx reverse proxy, while the Ingress controller acts as the virtual host configuration layer. The separation of concerns remains identical; only the infrastructure abstraction changes.
When combining MetalLB with cert-manager for TLS, ensure your DNS records point to the MetalLB-assigned IP, not to individual node IPs. This decouples your certificate lifecycle from node topology changes — critical when performing cluster upgrades or node replacements.
Deploying Kubernetes on Bare Metal with MetalLB in Production
Running Kubernetes on bare metal with MetalLB is a mature, production-viable strategy in 2026 for teams willing to own their networking layer. Start with Layer 2 for validation and non-critical workloads, then graduate to BGP when you need true horizontal scaling and sub-second failover. Document your IP allocations rigorously, monitor speaker pod health alongside your application metrics, and treat MetalLB configuration as infrastructure code subject to the same review process as your application deployments.
If you are evaluating bare-metal Kubernetes for a Nepal-based project or need assistance designing a resilient self-hosted architecture, reach out to discuss your infrastructure requirements. I help teams build and maintain production systems that balance operational control with practical maintainability.

