Why a Raspberry Pi homelab?
512 MB. That is all the headroom k3s leaves on a master with 8 GB of RAM once etcd and the kube-apiserver are running. The question comes up often: why put yourself through that constraint when cloud credits are available for free? Because constraint is the best teacher. On an RPi with 8 GB of RAM shared between etcd, the kube-apiserver and a dozen application pods, every megabyte counts. That constraint forces a deep understanding of what Kubernetes components actually consume.
This cluster was designed with a clear goal: reproduce the practices of a production cloud-native infrastructure, but on personal hardware, on a controlled budget.
Hardware architecture
The cluster is made up of 6 Raspberry Pi nodes:
| Node | Model | RAM | Role | IP |
|---|---|---|---|---|
| master-1 | RPi 4 | 8 GB | control-plane + etcd | 10.0.0.11 |
| master-2 | RPi 4 | 8 GB | control-plane + etcd | 10.0.0.12 |
| master-3 | RPi 4 | 8 GB | control-plane + etcd | 10.0.0.13 |
| worker-1 | RPi 5 | 16 GB | main worker | 10.0.0.21 |
| worker-2 | RPi 5 | 16 GB | main worker | 10.0.0.22 |
| worker-3 | RPi 4 | 2 GB | auxiliary worker | 10.0.0.23 |
The masters use 32 GB SD cards for the operating system, with NVMe SSDs over USB 3.0 for Longhorn storage. The RPi 5 workers have M.2 SSDs through the official HAT, which delivers noticeably better performance.
Network: every node is wired over Gigabit Ethernet through an unmanaged switch. IPs are fixed via DHCP reservations on the router. No VLAN in this version — simplicity first.
Choosing k3s
K3s won over kubeadm for several reasons:
- Reduced memory footprint: k3s uses ~512 MB on the masters versus ~1.5 GB for a standard kubeadm cluster.
- Single binary: one executable bundles every control-plane component. Fewer moving parts to manage.
- Built-in etcd: k3s ships an automatically managed etcd distribution, perfect for a 3-node HA setup.
- Native ARM64: official images for armv7 and arm64, no fiddling required.
The one notable limitation: k3s uses its own implementation of certain features (the datastore in particular), which can create slight divergences from a “vanilla” Kubernetes cluster. In practice, that has never caused a problem.
Bootstrapping the cluster
First master
# On master-1
curl -sfL https://get.k3s.io | sh -s - server \
--cluster-init \
--tls-san 10.0.0.11 \
--tls-san k3s.local \
--disable traefik \
--disable servicelb \
--node-label node-type=master
The --disable traefik option is intentional: Traefik will be installed through Helm with a custom configuration, not through the k3s bundle. --cluster-init initialises a new etcd cluster.
# Retrieve the token for the other nodes
sudo cat /var/lib/rancher/k3s/server/node-token
Additional masters
# On master-2 (adjust --tls-san per node: 10.0.0.12 or 10.0.0.13)
curl -sfL https://get.k3s.io | sh -s - server \
--server https://10.0.0.11:6443 \
--token <TOKEN> \
--tls-san 10.0.0.12 \
--disable traefik \
--disable servicelb \
--node-label node-type=master
Workers
# On each worker
curl -sfL https://get.k3s.io | K3S_URL=https://10.0.0.11:6443 \
K3S_TOKEN=<TOKEN> sh -s - agent \
--node-label node-type=worker
After a few minutes:
$ sudo kubectl get nodes
NAME STATUS ROLES AGE VERSION
master-1 Ready control-plane,etcd,master 5m v1.34.3+k3s1
master-2 Ready control-plane,etcd,master 4m v1.34.3+k3s1
master-3 Ready control-plane,etcd,master 4m v1.34.3+k3s1
worker-1 Ready <none> 3m v1.34.3+k3s1
worker-2 Ready <none> 3m v1.34.3+k3s1
worker-3 Ready <none> 2m v1.34.3+k3s1
High availability — how does it work?
HA in k3s rests on three mechanisms:
1. etcd quorum: with 3 masters, the cluster can survive the loss of one node. etcd requires (n/2)+1 members to reach quorum — that is 2 out of 3. Losing 2 masters at once interrupts the control-plane (existing workloads nevertheless keep running on the workers).
2. Distributed kube-apiserver: each master exposes the Kubernetes API. If a master fails, kubectl and the controllers fail over automatically to the remaining masters. The KUBECONFIG configuration points at one master’s IP, ideally behind a load balancer (not implemented here — a future improvement).
3. Self-healing: the kubelet on each node restarts crashed pods. k3s itself is configured as a systemd service with Restart=always.
API server répond API server responds
Les 3 masters votent. Quorum largement atteint. All 3 masters vote. Quorum comfortably reached.
API toujours dispo · requêtes re-routées API still available · requests re-routed
Quorum maintenu (2 ≥ (3/2)+1=2). Le control-plane survit. Quorum maintained (2 ≥ (3/2)+1=2). The control-plane survives.
Quorum perdu · control-plane interrompu Quorum lost · control-plane interrupted
Mais les pods déjà schedulés continuent de tourner sur les workers. But the already scheduled pods keep running on the workers.
2 sur 3.
On survit à 1 master perdu, pas à 2 : au-delà, le control-plane
s’arrête, mais les workloads déjà schedulés tiennent sur les workers.
Configuring kubectl locally
To avoid having to SSH in for every command:
# Copy the kubeconfig from master-1
scp [email protected]:/etc/rancher/k3s/k3s.yaml ~/.kube/config
# Change the server IP
sed -i 's/127.0.0.1/10.0.0.11/g' ~/.kube/config
chmod 600 ~/.kube/config
Lessons learned
Node labels from the start: the nodeSelector fields in the manifests depend on labels. Setting node-type=master and node-type=worker at install time avoids catching up later.
Disable the bundled components: k3s includes Traefik and ServiceLB by default. Disabling them at bootstrap (--disable traefik --disable servicelb) avoids conflicts when installing Helm versions.
Master storage: use SSDs even for the masters. An SD card degrades quickly under etcd load. A €20 USB 3.0 SSD changes everything.
worker-3 at 2 GB: this node turned out to be too limited to host substantial workloads. Today it mainly serves to raise the Longhorn quorum (3rd replica) for the volumes that need it. Never underestimate the minimum RAM required by the Kubernetes agents.
Going further
The logical next step: encrypt Kubernetes secrets before committing them into Git with Sealed Secrets, then automate deployments with ArgoCD.