Kokil Thapa - Professional Web Developer in Nepal
Freelancer Web Developer in Nepal with 15+ Years of Experience

Kokil Thapa is an experienced full-stack web developer focused on building fast, secure, and scalable web applications. He helps businesses and individuals create SEO-friendly, user-focused digital platforms designed for long-term growth.

Kubernetes on Bare Metal with MetalLB

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.

Physical RouterBGP / L2 SwitchMetalLB ControllerIP Allocation + ProtocolWorker Node 1speaker PodApp Pod (Port 80)Worker Node 2speaker PodApp Pod (Port 80)Worker Node 3speaker PodApp Pod (Port 80)Physical LAN Subnet (e.g., 192.168.10.0/24)
High-level architecture of Kubernetes on bare metal with MetalLB connecting physical routers to worker node speakers

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.

Start: Choose ModeDoes your router support BGP?NoYesLayer 2 ModeSimple ARP/NDP failoverSingle-node bottleneckBGP ModeTrue ECMP load balancingMulti-node traffic distributionBest: Dev/Test/Small ProdBest: Production Scale
Decision tree for selecting Layer 2 or BGP mode when configuring Kubernetes on bare metal with MetalLB

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.

CriteriaLayer 2 ModeBGP Mode
Router RequirementNone (standard L2 switch)BGP-capable router (MikroTik, VyOS, Cisco, Juniper)
Traffic DistributionSingle active node per IPECMP across all nodes
Failover Speed~10-30 seconds (ARP reannouncement)<1 second (BGP withdrawal)
Configuration ComplexityLow (two YAML files)Moderate (router BGP config + peer definitions)
Scalability CeilingLimited by single node NIC/CPUScales linearly with node count
Best ForDev/staging, small prod, non-BGP networksProduction 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, and nf_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 *.
Service Stuck in PendingCheck: kubectl describe svc <name>No EventsIP Alloc ErrorSpeaker DownController IssueCheck controller logsValidate CRD syntaxPool Exhausted / ConflictExpand IPAddressPool rangeVerify no DHCP overlapNode / Network IssueCheck speaker pod logsVerify firewall + modulesFix CRD / RestartAdd IPs / Exclude RangeOpen Ports / Load ModulesRe-test: kubectl get svc → External IP Assigned
Diagnostic workflow for resolving pending LoadBalancer services in Kubernetes on bare metal with MetalLB

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.

Frequently Asked Questions

MetalLB is a load balancer implementation for bare metal Kubernetes clusters that provides Network LoadBalancer services. Cloud providers automatically provision external IPs, but bare metal lacks this capability. MetalLB fills that gap by assigning IP addresses from a configured pool and handling traffic routing via Layer 2 or BGP protocols.

Layer 2 uses ARP/NDP to announce service IPs on the local subnet, requiring no router configuration but limiting traffic to a single node per service. BGP peers with upstream routers to advertise routes, enabling true multi-node load balancing and faster failover, though it requires compatible network hardware and BGP expertise to configure correctly.

Yes, this is the standard bare metal pattern. MetalLB assigns an external IP to the Ingress Controller Service of type LoadBalancer. Nginx then routes HTTP traffic based on host and path rules. MetalLB handles only L4 address assignment while nginx manages L7 routing, TLS termination, and application-level logic.

Three control plane nodes with 4 vCPU and 8GB RAM minimum, plus worker nodes sized for workload. You need dedicated switch ports for each node and a reserved IP range outside DHCP scope. For Nepal deployments, budget roughly NPR 150,000 to 300,000 per node for refurbished enterprise gear, or USD 1,100 to 2,200.

Create an IPAddressPool custom resource specifying the CIDR or individual addresses MetalLB can assign. Apply a L2Advertisement or BGPAdvertisement resource linking to that pool. Without the advertisement resource, MetalLB allocates IPs but never announces them to the network, causing silent service unreachability in production.

Common causes include exhausted IP pools, missing advertisement resources, namespace restrictions, or conflicting IP assignments. Check metallb-system logs with kubectl logs -n metallb-system -l app=metallb. Verify your IPAddressPool has available addresses and that no other service claims the same static IP via loadBalancerIP field.

Yes, when configured correctly. Layer 2 mode provides failover through leader election but only one node serves traffic at a time. BGP mode enables active-active distribution across nodes with sub-second failover. For legal-tech portals or eCommerce systems I have built, BGP mode with redundant routers provides the reliability clients expect.

Layer 2 failover takes several seconds as the new leader sends gratuitous ARP packets. BGP failover depends on router hold timers, typically 1-3 seconds. Cloud LBs often fail over in milliseconds. For business-critical applications like payment gateways or booking systems, test failover behavior under load before going live.

MetalLB itself does not handle webhooks, but it exposes the stable external IP that payment gateways require for callback URLs. Configure MetalLB to assign a static IP to your ingress service, then register that IP or its DNS name with eSewa or Khalti. Ensure TLS termination happens at ingress for secure webhook delivery.

Restrict IPAddressPool ranges to prevent IP squatting. Use RBAC to limit who can create LoadBalancer services. Enable network policies to isolate metallb-system namespace. Keep MetalLB updated; v0.14.x patches CVEs in speaker components. On Ubuntu servers, configure UFW rules allowing only necessary ports and block direct node access to service IPs.

Check speaker pod logs for ARP/BGP errors. Verify no duplicate IP conflicts using arping or ip neigh show. Confirm switch port configurations allow necessary protocols. Test from multiple clients to isolate network-segment issues. In my experience, most intermittent problems trace to misconfigured VLANs or aggressive ARP caching on intermediate switches.

Yes, MetalLB v0.14 supports IPv6 and dual-stack configurations. Define separate IPAddressPool resources for IPv4 and IPv6 ranges. Create corresponding advertisement resources for each family. Ensure your underlying network infrastructure, including switches and routers, properly handles IPv6 NDP or BGP announcements before attempting dual-stack setup.

Kube-VIP provides similar L2/BGP functionality with built-in ingress capabilities. Cilium offers native load balancing with eBPF. Keepalived with HAProxy works outside Kubernetes. NodePort avoids external dependencies but exposes non-standard ports. Choose based on operational complexity tolerance; MetalLB remains the most documented option for teams new to bare metal networking.

Three-node bare metal cluster costs NPR 450,000-900,000 upfront plus NPR 5,000-10,000 monthly for power and internet. Equivalent cloud resources cost NPR 30,000-60,000 monthly. Break-even occurs around 12-18 months. For long-running projects like legal portals or directories, bare metal saves significantly after initial investment, assuming you have Linux administration capability.

Skip MetalLB if running single-node development clusters where NodePort suffices. Avoid it when your network team cannot support BGP and Layer 2 limitations are unacceptable. Do not use it behind NAT without understanding hairpin routing complexities. For temporary prototypes or client demos, cloud-managed Kubernetes eliminates networking overhead entirely.

Share this article

Quick Contact Options
Choose how you want to connect me: