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.

Homelab Kubernetes, Episode 2: MetalLB

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 nodes shows Ready
  • 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.