After getting WireGuard running on a Raspberry Pi, the natural next homelab project is Kubernetes. Running full upstream Kubernetes on a Pi is painful — kubeadm wants a beefy etcd cluster, multiple control-plane binaries, and RAM you don’t have. k3s solves this: it’s a single ~70 MB binary, CNCF-certified, that ships as one process per node, uses SQLite instead of etcd by default on single-server setups, and bundles containerd, Traefik, CoreDNS and a basic load balancer out of the box. It’s built by Rancher/SUSE specifically for edge, IoT and ARM devices, which makes it the obvious choice for a Raspberry Pi cluster.
This post walks through building a k3s cluster on a handful of Raspberry Pis: system prep, installing the server (control plane) node, joining agent (worker) nodes, verifying the cluster, deploying a test workload, and exposing it with MetalLB.
What you need
- At least 1 Raspberry Pi as the server (control plane) node, ideally 2+ agent (worker) nodes — 3 or more boards total gives you a much more realistic cluster to play with.
- Raspberry Pi 4 or Pi 5, with at least 4 GB of RAM. k3s itself only needs 2 GB on the server and 512 MB on agents, but real workloads (and the OS) eat into that fast, so 4 GB+ boards are worth it.
- Raspberry Pi OS Lite (64-bit) on every node — k3s requires a 64-bit kernel; don’t use the 32-bit image.
- Boot from an SSD via USB if you can. SD cards work but are the most common source of flaky, hard-to-diagnose cluster problems long-term.
- A wired network (a small switch) is strongly recommended over Wi-Fi for cluster traffic.
- Static IPs or DHCP reservations for every node, and SSH access to all of them.
Step 1: Prepare every node
Do this on every Pi — the server and every agent. First, update the OS and firmware:
sudo apt update && sudo apt full-upgrade -y
sudo rpi-eeprom-update -a
sudo reboot
k3s needs the memory cgroup to be enabled to start the kubelet, and stock Raspberry Pi OS doesn’t enable it by default. Edit the boot cmdline file — on current Raspberry Pi OS (Bookworm and later) it’s under /boot/firmware/; on older Debian 11-based images it’s /boot/cmdline.txt:
sudo nano /boot/firmware/cmdline.txt
This file is a single line — do not add a newline. Append the following two parameters, separated by a space, to the end of the existing line:
cgroup_memory=1 cgroup_enable=memory
Save, then reboot for it to take effect:
sudo reboot
After the reboot, confirm the memory cgroup controller is actually active:
cat /proc/cgroups | grep memory
If the fourth column (enabled) shows 1, you’re good. Also give each Pi a distinct, resolvable hostname (sudo raspi-config → System Options → Hostname), since k3s uses the hostname as the node name by default.
Step 2: Install k3s on the server node
Pick one Pi to be the server (control plane). The official install script auto-detects the architecture and pulls the latest stable release — at the time of writing that’s the 1.34/1.35 line, but you can pin an exact version with INSTALL_K3S_VERSION if you want reproducible installs. A plain install looks like this:
curl -sfL https://get.k3s.io | sh -
If you plan to install MetalLB later (recommended — see below), disable the bundled service load balancer and Traefik up front so they don’t fight with it, and make the kubeconfig world-readable so your own user can run kubectl without sudo:
curl -sfL https://get.k3s.io | sh -s - server \
--write-kubeconfig-mode 644 \
--disable servicelb \
--disable traefik
To pin an exact version instead of “whatever is latest today”:
curl -sfL https://get.k3s.io | INSTALL_K3S_VERSION=v1.34.1+k3s1 sh -
This installs k3s as a systemd service (k3s.service), starts the control plane, generates TLS certs, and drops a kubeconfig at /etc/rancher/k3s/k3s.yaml. It also installs kubectl, crictl, and an uninstall script (/usr/local/bin/k3s-uninstall.sh) for free.
Check the service is healthy:
sudo systemctl status k3s
sudo k3s kubectl get nodes
If you want to run kubectl without sudo k3s in front of it, copy the kubeconfig into your own user’s home:
mkdir -p ~/.kube
sudo k3s kubectl config view --raw > ~/.kube/config
chmod 600 ~/.kube/config
Step 3: Join the worker (agent) nodes
Back on the server node, grab the node token — agents need it to authenticate to the cluster:
sudo cat /var/lib/rancher/k3s/server/node-token
On each worker Pi, install k3s in agent mode, pointing it at the server’s IP (or hostname) on port 6443 and passing the token you just copied:
curl -sfL https://get.k3s.io | K3S_URL=https://192.168.1.10:6443 \
K3S_TOKEN=K10abc...your-node-token... sh -
Replace 192.168.1.10 with your server’s actual address. This installs and starts k3s-agent.service instead of the full server. Repeat on every worker board. If two nodes end up with the same hostname (common when cloning SD card images), override it explicitly with K3S_NODE_NAME to avoid the second node silently colliding with the first in the cluster’s node list.
Step 4: Verify the cluster
Back on the server node (or any machine with the kubeconfig copied off it):
kubectl get nodes -o wide
You should see the server plus every agent listed with STATUS Ready. Also worth checking that system pods came up clean:
kubectl get pods -A
If a node is stuck NotReady, the usual culprit on a Pi is the cgroup step from Step 1 not actually being applied — double-check /proc/cgroups on that specific node and look at sudo journalctl -u k3s-agent -f for errors.
Step 5: Deploy something to prove it works
A minimal nginx Deployment plus a Service is enough to confirm scheduling, networking and DNS all work across nodes:
kubectl create deployment nginx --image=nginx
kubectl scale deployment nginx --replicas=3
kubectl get pods -o wide
Check that the three replicas landed on different nodes (k3s’s default scheduler will spread them), and expose the Deployment as a Service:
kubectl expose deployment nginx --port=80 --type=ClusterIP
kubectl run curl-test --rm -it --image=busybox --restart=Never -- \
wget -qO- nginx
If that returns the nginx welcome page HTML, pod networking, kube-proxy and CoreDNS are all working correctly across your cluster.
Step 6: Exposing services — Traefik or MetalLB
k3s ships with two networking conveniences enabled by default: Traefik as an Ingress controller, and a simple built-in ServiceLB (formerly “Klipper”) that lets type: LoadBalancer Services get an external IP without any extra components — it just binds the node’s own IP and forwards traffic with iptables. That’s genuinely enough for a lot of homelab use cases: if you only need HTTP(S) routing to a handful of apps, an Ingress resource behind Traefik is the path of least resistance and needs nothing extra installed.
Where ServiceLB falls short is when you want real, stable, dedicated IPs from your LAN’s address range handed out to Services — e.g. so a Service gets its own IP on your network rather than sharing a node’s IP. That’s what MetalLB is for, and it’s the standard bare-metal LoadBalancer implementation for exactly this scenario. If you installed k3s with --disable servicelb in Step 2, install MetalLB now:
kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/v0.16.1/config/manifests/metallb-native.yaml
Wait for its pods to come up in the metallb-system namespace, then configure an address pool from your LAN range (pick a block your router/DHCP server won’t hand out) and enable Layer 2 mode:
cat <
Now re-expose the nginx Deployment as a real LoadBalancer Service and MetalLB will hand it a dedicated IP from that pool:
kubectl expose deployment nginx --port=80 --name=nginx-lb --type=LoadBalancer
kubectl get svc nginx-lb
The EXTERNAL-IP column should fill in with an address from 192.168.1.240-192.168.1.250 within a few seconds — curl it from any machine on the LAN and you're serving traffic through a Raspberry Pi Kubernetes cluster.
Where to go from here
- Add persistent storage —
local-path-provisionerships with k3s by default, but for anything that needs to survive a node dying, look at Longhorn or an NFS-backed StorageClass. - Wire the cluster into the WireGuard VPN from the earlier post so you can reach it (and its Ingress/LoadBalancer IPs) remotely.
- Point Prometheus/Grafana at it — the kube-prometheus-stack Helm chart runs fine on k3s once you're comfortable with the basics above.
- Automate node provisioning with Ansible so re-imaging an SD card and rejoining the cluster is a single playbook run.