Homelab Kubernetes, Episode 2: MetalLB
Learn how to give your K3s cluster real LAN IP addresses using MetalLB. This guide explains Layer 2 networking, IP pools, LoadBalancer services, and prepares your homelab for Traefik and other self-hosted services.
This is part of a series on building a self-hosted Kubernetes homelab. Start with the series intro if you haven't already.
When you create a Kubernetes service some work better with a dedicated IP address and not simply share the same address as the host or every other service. The kubernetes system needs somewhere to get that IP from. On cloud providers like AWS or GCP, the cloud assigns one automatically. On a bare-metal machine in your house, nothing does that by default.
MetalLB solves this. It watches for services that want an external IP and assigns one from a pool of addresses you define. No cloud required.
Without MetalLB, Traefik (the reverse proxy covered in Episode 3) has no IP to bind to, and nothing else in this series works. This is a short guide, but it's critical.
Prerequisites
- k3s is installed and
kubectl get nodesshowsReady - Helm is installed (covered in guide 1)
- You've chosen four IP addresses or small ranges that are outside your router's DHCP range and not assigned to any other device
How MetalLB Works
MetalLB operates in Layer 2 mode for home networks. Layer 2 is IP addresses on a single network, not between networks. That is Layer 3. In Layer 2 mode, MetalLB responds to ARP requests on your LAN for the IP addresses in its pool.
ARP is how devices on a local network ask "who has this IP address?" When your router or another device asks "who has 192.168.1.91?", MetalLB answers on behalf of the cluster, and traffic flows to Traefik.
You define one or more IP address pools. When a Kubernetes service of type LoadBalancer is created, MetalLB assigns it an IP from the pool and advertises it on the network.
For this series, you'll define four pools:
| Pool | IP(s) | Service |
|---|---|---|
traefik-pool |
192.168.1.91 | Traefik (primary ingress) |
pihole-pool |
192.168.1.53 | Pi-hole (port 53 is DNS) |
plex-pool |
192.168.1.92 | Plex (needs its own IP for DLNA) |
first-pool |
192.168.1.94-99 | General use, auto-assigned |
Traefik gets a dedicated IP because it's the entry point for almost all other services. Pi-hole uses .53 because port 53 (DNS) can only bind once per IP. Plex gets its own IP so DLNA device discovery works correctly on the LAN.
Replace these IPs with unused addresses in your subnet.
Install MetalLB
MetalLB installs via Helm:
helm repo add metallb https://metallb.github.io/metallb
helm repo update
helm install metallb metallb/metallb \
--namespace metallb-system \
--create-namespace \
--wait
The --wait flag tells Helm to block until MetalLB is fully running before returning. This prevents the next step from failing because the MetalLB controller isn't ready yet.
After this completes, verify the pods are running:
kubectl get pods -n metallb-system
You should see the MetalLB controller and speaker pods in Running state.
Create the Manifest
Create /data/manifests/metallb.yaml:
# IP Assignment Reference
# 192.168.1.53 Pi-hole (pihole-pool)
# 192.168.1.91 Traefik (traefik-pool)
# 192.168.1.92 Plex (plex-pool)
# 192.168.1.94-99 General (first-pool)
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
name: first-pool
namespace: metallb-system
spec:
addresses:
- 192.168.1.94-192.168.1.99
---
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
name: traefik-pool
namespace: metallb-system
spec:
addresses:
- 192.168.1.91/32
---
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
name: pihole-pool
namespace: metallb-system
spec:
addresses:
- 192.168.1.53/32
---
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
name: plex-pool
namespace: metallb-system
spec:
addresses:
- 192.168.1.92/32
---
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
name: l2-advert
namespace: metallb-system
spec:
ipAddressPools:
- first-pool
- traefik-pool
- pihole-pool
- plex-pool
The L2Advertisement object is what actually enables Layer 2 mode. Without it, MetalLB knows about the pools but won't advertise them on the network. Both the pool and the advertisement need to exist.
Apply it:
kubectl apply -f /data/manifests/metallb.yaml
Verify the Pools
Check that MetalLB accepted your configuration:
kubectl get ipaddresspools -n metallb-system
You should see all four pools listed. If any show an error, check the MetalLB controller logs:
kubectl logs -n metallb-system deploy/metallb-controller
At this point, no services are using these IPs yet. The real test comes in the Traefik guide, when you'll see Traefik's service get assigned 192.168.1.91 automatically.
How Services Request a Specific Pool
Later in the series, each manifest requests a specific pool using an annotation. Here's an example from the Traefik manifest (you don't need to write this now):
apiVersion: v1
kind: Service
metadata:
name: traefik
namespace: traefik
annotations:
metallb.io/address-pool: traefik-pool
spec:
type: LoadBalancer
...
The annotation metallb.io/address-pool: traefik-pool tells MetalLB which pool to pull the IP from. Services without this annotation get an IP from first-pool.
Common Problems
Never let two pools overlap. If you add a dedicated pool for a new service, remove that IP from first-pool in the same edit. If the same IP appears in two pools, MetalLB may assign it to two services, causing a conflict where neither works reliably.
For example, if you want to give 192.168.1.94 to a new service, change first-pool from 192.168.1.94-192.168.1.99 to 192.168.1.95-192.168.1.99 at the same time you add the new dedicated pool.
MetalLB requires at least one L2Advertisement. Defining pools alone is not enough. The L2Advertisement in the manifest above covers all pools. If you ever delete and reapply the manifest piecemeal, make sure the advertisement object is also applied.
Summary
MetalLB is running and four IP pools are configured. Any service of type LoadBalancer created in the cluster can now receive a real LAN IP.
In Guide 3, you'll install Traefik. It picks up the 192.168.1.91 IP from MetalLB, handles TLS termination, and routes traffic to every service in the cluster based on hostname.