Containers

Raspberry Pi 5 / CM5 k3s Cluster: Turing Pi vs PoE vs DeskPi

This post contains affiliate links. If you buy through them, we may earn a small commission at no extra cost to you. Learn more.

Most Raspberry Pi cluster guides assume a Compute Module 5 drops into any CM4 carrier board. That assumption no longer holds. Turing Pi doesn’t list the CM5 for the Turing Pi 2.5, and DeskPi says plainly that the Super6C won’t take it. Even the official PoE+ HAT is listed for the Pi 4 and 3B+ only. So if you’re building a Raspberry Pi k3s cluster this year, the module decides the board, not the other way round.

Original content from computingforgeeks.com - post 177924

We compare the three builds people shortlist: a Turing Pi 2.5, a Raspberry Pi 5 stack powered over PoE, and DeskPi’s Super6C with its CM5 sibling, the Super4C. Each gets a parts list with price bands. Then we bring up a three-server k3s cluster with embedded etcd and kill a node to watch what happens. The k3s behavior described here was checked against the current stable release in October 2026 on three lab nodes.

How the three Pi cluster builds compare

The PoE stack is the only build that runs Raspberry Pi 5 silicon today on boards you can actually buy. The Super4C is the CM5 cluster board, and the Turing Pi 2.5 earns its price through the BMC rather than the modules it takes. The table puts the deciding specs side by side, and the sections after it explain why each row matters.

BuildNodesModules it officially takesStorage per nodeNetworkPower inBoard or core cost
Turing Pi 2.54CM4 (via adapter), Turing RK1, JetsonM.2 NVMe for RK1; mPCIe, SATA or USB for CM4Onboard 1GbE managed switch, VLAN24-pin ATXabout $280 board, direct only
Pi 5 PoE stack3 to 4Raspberry Pi 5 (any RAM size)NVMe on a PoE + M.2 HAT, PCIe Gen 2 x1Separate PoE+ switchPoE+ over the Ethernet cableabout $175 per 8GB board plus about $30 per HAT
DeskPi Super6C6CM4 onlyM.2 2280 NVMe, PCIe Gen 2 x1Onboard 8-port gigabit switchDC barrel (19 V supply included) or 4-pin ATXabout $160 to $170
DeskPi Super4C4CM5 onlyM.2 2280 NVMe1GbE per node, 2.5GbE via USB 3.012 to 20 V DC, supply includedabout $205 to $220

Two rows look similar and are not. The Super6C and the Super4C share a vendor and a form factor, but they are different boards for different module generations, and a CM5 bought for the Super6C will not boot there. The Turing Pi row is the other trap. With CM4 modules its four M.2 slots do nothing, because the docs mark M.2 NVMe as unsupported for CM4, so etcd lives on SATA, USB or mPCIe storage.

How we checked these builds

We did not have Pi hardware on the bench for this one, so the guide separates what was measured from what was read. The hardware sections are spec-sheet work: every board, HAT and switch was checked against the manufacturer’s page or wiki, a live retailer listing and, where one existed, a forum or third-party report. Prices are bands from late October 2026, and the RAM and NAND shortage is still moving them, so check the live price at each link.

k3s is the measured half. We ran the exact bring-up below on three Ubuntu 26.04 VMs named node-a, node-b and node-c, standing in for the Pi nodes. k3s ships the same install script and the same embedded etcd on arm64 and x86, so the join flow, the port list, the ServiceLB behavior and the failover timing carry over. What does not carry over is anything Pi specific: the cgroup flag, NVMe boot and thermals come from the Raspberry Pi and K3s documentation, and we say so where they appear. We publish no Pi benchmark numbers because we did not measure any.

Why the CM5 does not fit every cluster board

The CM5 keeps the CM4’s two 100-pin connectors, which is why people expect it to be a drop-in part. The catch is in what the carrier routes to those pins. A board designed around the CM4 makes assumptions about PCIe, USB and boot signalling that the CM5 changes, so a module can physically seat and still fail to boot, or boot without some of its I/O. The vendors’ own statements are the only reliable guide here:

  • Turing Pi 2.5: the product page lists Turing RK1, CM4 and Jetson modules, and the CM4 adapter says it is for the CM4. Forum reports through January 2026 describe a CM5 in the CM4 adapter booting only in slots 1 and 2, with no flashing through the BMC.
  • DeskPi Super6C: the DeskPi wiki marks the CM5 as not supported and points CM5 owners to the Super4C.
  • DeskPi Super4C: sold as compatible with the CM5 only, so it is the reverse case. A drawer full of CM4 modules does not go here.
  • Raspberry Pi PoE+ HAT: the official HAT’s product page lists the Pi 4 and 3B+, so a Pi 5 PoE stack needs a third-party HAT such as the Waveshare boards below.

So decide the module first. If you want CM5 or Pi 5 silicon, your choices are the PoE stack or the Super4C. If you already own CM4 modules, the Super6C or a Turing Pi 2.5 puts them to work. Buying a Turing Pi 2.5 for CM5s you plan to get later is the one path we would steer you away from, because nothing on Turing Pi’s own pages promises it will work.

Build 1: Turing Pi 2.5, four nodes behind a BMC

The Turing Pi 2.5 is a Mini-ITX board with four module slots, a 1GbE managed switch with VLAN support and a baseboard management controller that can power-cycle and flash each node from its own CLI, tpi. That BMC is what you pay for. On the other builds a hung node means walking over and pulling a cable, while here it means tpi power from your desk, which matters more than it sounds once the cluster sits in a cupboard.

Turing Pi 2.5 Mini-ITX cluster board with four compute module slots and BMC
Turing Pi 2.5: four slots, onboard 1GbE switch, BMC and 24-pin ATX power. Sold direct by Turing Pi. Image: Turing Pi.

The I/O is not symmetric, and that shapes where k3s roles land. Per Turing Pi’s docs, node 1 gets the 40-pin GPIO, a mini PCIe slot and HDMI, node 2 another mini PCIe slot, node 3 the two SATA ports and node 4 the four USB 3.0 ports. Each node also has an M.2 slot, but only RK1 modules use it (PCIe 3.0 x4); with CM4 modules the docs list M.2 NVMe as not supported. On a CM4 build that pushes the etcd disks onto SATA for node 3 and USB for node 4. We’d run one server and three agents on CM4, or buy RK1 modules if you want three proper etcd members.

PartPickPrice bandNote
BoardTuring Pi 2.5about $280, directNo Amazon listing found
Module adapterTuring Pi CM4 adapter (x4)about $12 eachCM4 only; has a microSD slot for CM4 Lite
ModulesRaspberry Pi CM4, or Turing RK1RK1 about $250 to $380 by RAMRK1 was out of stock when we checked
PowerTuring Pi 24-pin pico PSUabout $45, directOr any ATX supply you trust
StorageSATA SSD on node 3, USB SSD on node 4variesNVMe only with RK1 modules

Buy this build if you already own CM4 modules or want RK1 performance and value remote power and flashing over raw node count. Skip it if your plan is Raspberry Pi 5 class silicon on official support, because that is exactly the configuration Turing Pi does not list today.

Build 2: Raspberry Pi 5 stack powered over PoE

The PoE stack is the most flexible of the three because every node is a standard Raspberry Pi 5. A dead board is a same-day replacement from any reseller, and you can grow the cluster one node at a time. Power and network share one cable per node, which removes four USB-C bricks and a power strip from the shelf. It is also the only build here where every part is current Raspberry Pi silicon with the vendor’s own support behind it.

Raspberry Pi 5 board used as a k3s cluster node
Raspberry Pi 5 8GB: quad-core Cortex-A76, PCIe 2.0 x1, PoE+ capable Ethernet jack. Image: Raspberry Pi.

Pick the 8GB board for k3s servers. Our three-server lab cluster used 709 to 1,088 MiB per node with only the system pods and a small nginx test running. A 4GB board works for agents but leaves little room once you add monitoring, and the 16GB board costs about $305 now, which is hard to justify for a learning cluster. The 8GB board runs about $175 at approved resellers such as PiShop, while the Amazon listing linked above comes from a third-party seller at a markup, so compare before you buy.

Since the official PoE+ HAT is not listed for the Pi 5, the HAT choice is Waveshare’s. We’d build around the PoE M.2 HAT+ (B), because it puts 802.3af/at power and an M.2 slot for 2230 to 2280 NVMe drives on one board. Waveshare also lists it as compatible with the official Active Cooler underneath. Its GPIO header delivers up to 4.5 A at 5 V (Waveshare’s product page rates 25 W across its 5 V and 12 V outputs), so the Pi treats it as a sub-5 A supply and caps USB current at 600 mA. That matters only if you hang USB drives off the nodes, and Waveshare’s wiki lifts the cap with usb_max_current_enable=1 in config.txt. Stacking a separate PoE HAT on top of a separate M.2 HAT is where most Pi 5 stacks run into clearance and FPC cable problems, and the combined board avoids that entirely.

Waveshare PoE M.2 HAT+ B combining PoE power and an NVMe slot for Raspberry Pi 5
Waveshare PoE M.2 HAT+ (B): 802.3af/at PoE, 5V 4.5A on the GPIO header (25 W total), M.2 2230 to 2280 NVMe. Image: Waveshare.

The switch is where PoE stacks quietly fail. TP-Link rates the TL-SG1005P at 30 W per port but 65 W across all four PoE+ ports, which works out to about 16 W per Pi when four nodes draw at once. A Pi 5 with an NVMe drive and a fan can exceed that at peak, and TP-Link says the switch then cuts power to the lower-priority port. In a cluster that means a node dropping off without warning, the exact failure the drill below simulates. We’d run three nodes on this switch and keep the fourth PoE port as headroom, or buy a bigger-budget switch for a four-node stack.

TP-Link TL-SG1005P five-port gigabit switch with four PoE+ ports for a Raspberry Pi cluster
TP-Link TL-SG1005P: 5 gigabit ports, 4 of them PoE+ at 30 W each, 65 W total budget. Image: TP-Link.

A three-node stack needs these parts, and a fourth node simply repeats the per-node rows:

PartPickPrice bandNote
Nodes (x3)Raspberry Pi 5 8GBabout $175 at resellers, more on Amazon16GB is about $305
PoE + NVMe (x3)Waveshare PoE M.2 HAT+ (B)about $24 to $31Active Cooler fits underneath
NVMe (x3)Patriot P320 256GB 2280about $55 to $60Runs at PCIe Gen 2 x1 on the Pi 5 by default
Cooling (x3)Raspberry Pi Active Coolerabout $11Official part; skip it if your case ships its own coolers
SwitchTP-Link TL-SG1005Pabout $35 to $4565 W total PoE budget
Setup cardSanDisk Extreme 64GB A2 microSDabout $30 to $35One card, used once per node to write the OS to NVMe
FrameGeeekPi 4-layer Pi 5 cluster caseabout $34Includes four coolers; we could not confirm it clears a PoE HAT, so check layer height first

Buy this build if you want Pi 5 nodes today, parts you can replace from any shop, and a cluster that grows a node at a time. Skip it if cable count and shelf space matter more to you than flexibility, because a PoE stack is still four boards, four HATs and a switch, where the cluster boards below are one Mini-ITX tray. If you want the switch to serve cameras and access points too, our PoE switch guide covers bigger budgets.

Build 3: DeskPi Super6C for CM4, Super4C for CM5

DeskPi’s boards trade the BMC for density and price. The Super6C packs six CM4 slots onto a Mini-ITX board with an onboard 8-port gigabit switch. Each slot has its own M.2 2280 NVMe socket at PCIe Gen 2 x1, which makes it a better etcd platform than a CM4 Turing Pi. Power comes in through a DC barrel jack or a 4-pin ATX connector, and DeskPi bundles a 19 V supply.

DeskPi Super6C Mini-ITX cluster board with six Raspberry Pi CM4 slots
DeskPi Super6C: six CM4 slots, M.2 2280 per node, onboard 8-port gigabit switch. CM4 only. Image: DeskPi.

The Super6C’s weak spot is provisioning. According to DeskPi’s wiki, only slot 1 has the micro USB port for flashing eMMC with rpiboot. A six-node cluster of eMMC modules means a slow afternoon of moving modules through slot 1, which is a good reason to buy CM4 Lite modules. Booting them from NVMe also needs an EEPROM boot order that includes NVMe, and updating that EEPROM goes through rpiboot in slot 1 as well. There is no BMC either, so a hung node is a trip to the power switch.

The Super4C is the board DeskPi points CM5 owners to. It holds four CM5 modules and gives each one an M.2 2280 slot, its own 1GbE port and a 2.5GbE port through USB 3.0. An onboard ESP32 switches power per node and reads per-channel power through an INA3221 sensor. That ESP32 is a small step toward what the Turing Pi BMC does, without the remote flashing.

DeskPi Super4C cluster board with four Raspberry Pi CM5 slots
DeskPi Super4C: four CM5 slots, NVMe, 1GbE plus USB 2.5GbE per node, ESP32 power control. Image: DeskPi.

Module supply is the problem with any CM5 build right now. When we checked, every CM5 variant at PiShop, an approved Raspberry Pi reseller, was out of stock, and the Amazon listings came from third-party sellers well above list. Raspberry Pi’s list prices start at about $68 for a 2GB Lite module, and PiShop lists an 8GB module with 32GB eMMC at about $200. The CM5 datasheet also says the module needs a thermal solution, so budget a heatsink per node.

Raspberry Pi Compute Module 5 for a CM5 Kubernetes cluster
Raspberry Pi Compute Module 5: 2 to 16GB RAM, Lite or 16 to 64GB eMMC. Buy from an approved reseller. Image: Raspberry Pi.

Both DeskPi parts lists look like this:

PartSuper6C build (CM4)Super4C build (CM5)
BoardDeskPi Super6C, about $160 to $170DeskPi Super4C, about $205 to $220
ModulesUp to 6x CM4 (CM4 Lite boots from microSD, or NVMe once its EEPROM boot order includes NVMe)Up to 4x CM5, 8GB recommended for servers
StoragePatriot P320 256GB 2280 per node, about $55 to $60Same drive, 2280 slot per node
PowerBundled 19 V supply, or 4-pin ATXBundled 19 V supply
CaseGeeekPi ITX case kit with fan, about $63DeskPi Super4C metal case, about $29 direct
SwitchOnboardAny 5-port gigabit switch; the board has no onboard switch
CoolingCM4 heatsink per moduleCM5 heatsink per module (the datasheet requires one)

Buy the Super6C if you have CM4 modules and want six nodes on one tray with NVMe for each. Buy the Super4C if you are set on CM5 and can find modules. Skip both if you need remote power and flashing, because neither board has a BMC like the Turing Pi’s.

Why etcd needs NVMe and not a microSD card

Every server node in an HA k3s cluster runs an etcd member, and etcd writes to disk and syncs on every change to cluster state. K3s’s own requirements page is blunt about it: on a Raspberry Pi it recommends an external SSD because “SD cards and eMMC cannot handle the IO load”. When a disk cannot keep up, etcd misses heartbeats, members drop out and the API starts timing out, which looks like a networking fault but is a storage one.

That is why each build above puts NVMe on the server nodes. On the Pi 5 and CM5, PCIe runs at Gen 2 x1 by default. Raspberry Pi’s documentation warns that Gen 3 is not certified and may be unstable, so leave it at Gen 2 for a cluster you want to trust. Gen 2 x1 is still far faster for small synced writes than any microSD card. Our etcd data directory held 132 MB after the lab cluster settled, so a 256GB drive leaves plenty of room for container images and local-path volumes.

How the Raspberry Pi k3s cluster fits together

We run all three nodes as k3s servers with embedded etcd rather than one server and two agents. With three etcd members the cluster keeps quorum when any single node dies, because two of three votes is still a majority. With one server, losing that node takes the API down even though the agents keep their pods running. The K3s docs call for an odd number of servers, three or more, for exactly this reason.

Raspberry Pi k3s cluster architecture diagram: workstation kubectl to node-a, node-b and node-c on 6443/TCP, all three servers sharing embedded etcd on 2379-2380

The diagram uses the addresses from our lab, node-a on 10.0.1.11, node-b on 10.0.1.12 and node-c on 10.0.1.13. Swap in your own. These are the ports k3s listens on, confirmed with ss on node-a:

  • 6443/TCP: the Kubernetes API and k3s supervisor, which agents, joining servers and your kubectl all talk to.
  • 2379-2380/TCP: etcd client and peer traffic between servers, listening on the node’s own address.
  • 10250/TCP: the kubelet, used for logs, exec and metrics.
  • 8472/UDP: flannel VXLAN, the pod network between nodes. K3s’s docs say never to expose it to the internet.

Keep the cluster on its own switch or VLAN and none of these ports needs to leave it. The Turing Pi’s managed switch can do that VLAN on the board itself.

What each Raspberry Pi node needs before k3s

Each node needs Raspberry Pi OS Lite (64-bit) on fast storage, a unique hostname and a static DHCP lease, because k3s identifies nodes by hostname and the join command points at node-a’s IP. On a Turing Pi the BMC flashes nodes, and on the Super6C and the CM5 boards you use rpiboot from Raspberry Pi’s usbboot tools. A Pi 5 on the Waveshare HAT is the odd one out, since nothing can write to its NVMe until the Pi itself is running.

We could not run this part without Pi hardware, so the sequence below follows Raspberry Pi’s PCIe and boot documentation and Waveshare’s wiki for this HAT, in order:

  1. Boot once from microSD: flash Raspberry Pi OS Lite to the setup card and boot the node from it with the HAT and NVMe fitted.
  2. Enable the PCIe connector: add dtparam=pciex1 to /boot/firmware/config.txt. Waveshare’s wiki tells you to, because the connector stays off unless a HAT+ device is detected, then reboot and check that ls -l /dev/nvme* shows the drive.
  3. Set the boot order: run sudo rpi-eeprom-config --edit and set BOOT_ORDER=0xf416 plus PCIE_PROBE=1, which Raspberry Pi’s docs require for booting from a device that is not HAT+.
  4. Write the OS to the NVMe: on your workstation, put the drive in a USB NVMe enclosure and flash Raspberry Pi OS Lite with Raspberry Pi Imager, setting the user, hostname and SSH key in its customisation screen. Add dtparam=pciex1 to config.txt on the drive’s boot partition as well, then fit the drive, remove the microSD card and boot.

One microSD card and one USB NVMe enclosure do all the nodes, which is why the parts list carries a single card. Leave PCIe at its default Gen 2 setting for the reasons in the storage section above.

Raspberry Pi OS does not start with the memory cgroup enabled, and k3s will not start without it. The K3s docs fix this by appending two flags to the single line in /boot/firmware/cmdline.txt, which must stay one line. This sed appends them to the end of that line:

sudo sed -i '$ s/$/ cgroup_memory=1 cgroup_enable=memory/' /boot/firmware/cmdline.txt

We ran that exact sed against a copy of a stock Raspberry Pi OS cmdline line, since our lab nodes are not Pis, and it left one line ending in the two flags:

console=serial0,115200 console=tty1 root=PARTUUID=58b06195-02 rootfstype=ext4 fsck.repair=yes rootwait cgroup_memory=1 cgroup_enable=memory

Reboot, then confirm the memory controller is present:

cat /sys/fs/cgroup/cgroup.controllers

This is the output from our Ubuntu lab VMs. A Pi kernel prints a slightly different list, but memory must be in it either way:

cpuset cpu io memory hugetlb pids rdma misc dmem

If memory is missing, the flags did not land on the kernel command line, and cat /proc/cmdline shows what the kernel actually booted with.

Raspberry Pi OS Lite ships without a firewall, and K3s’s own first recommendation is to leave ufw disabled. If you do run it, open the k3s ports to the cluster’s LAN only (10.0.1.0/24 here) and allow the pod and service networks, 10.42.0.0/16 and 10.43.0.0/16 by default. We applied this exact ruleset on all three lab nodes with the cluster running, and the nodes stayed Ready:

sudo apt install -y ufw
sudo ufw allow 22/tcp
sudo ufw allow from 10.0.1.0/24 to any port 6443 proto tcp
sudo ufw allow from 10.0.1.0/24 to any port 2379:2380 proto tcp
sudo ufw allow from 10.0.1.0/24 to any port 10250 proto tcp
sudo ufw allow from 10.0.1.0/24 to any port 8472 proto udp
sudo ufw allow from 10.42.0.0/16
sudo ufw allow from 10.43.0.0/16
sudo ufw --force enable

K3s’s requirements page lists the two pod and service network rules alongside the ports, and scoping the rest to the LAN keeps the VXLAN port off the internet, which the docs warn against. One finding surprised us: ufw does not gate ServiceLB ports. The test service on port 8080, set up later in this guide, answered a curl from a machine outside the cluster with no 8080 rule in place, because the DNAT rules that ServiceLB’s hostPort and kube-proxy install in the PREROUTING chain send that traffic through FORWARD to the pod, so ufw’s input rules never see it. Treat a LoadBalancer port as public on the LAN whatever ufw says. The opposite trap applies to 6443: with it scoped to 10.0.1.0/24, a workstation on a VPN or another VLAN loses kubectl once ufw is enabled, so add its subnet before you run the last command.

How the first k3s server starts etcd

All three servers must share one secret token, so generate it once on node-a and keep it, because the other two nodes need the same value to join. Set the first server’s address in a variable too, since it repeats in every join command:

export K3S_TOKEN=$(openssl rand -hex 16)
export SERVER_IP=10.0.1.11
echo "${K3S_TOKEN}"

Install the first server with --cluster-init, which starts embedded etcd instead of the default SQLite datastore. The --tls-san flag adds an extra name to the API certificate (k3s already includes the node’s own IPs, so it is belt and braces here, and essential if you later front the servers with a VIP), and --write-kubeconfig-mode=644 lets your user run kubectl without sudo:

curl -sfL https://get.k3s.io | K3S_TOKEN="${K3S_TOKEN}" sh -s - server --cluster-init --tls-san="${SERVER_IP}" --write-kubeconfig-mode=644

Our node finished the install in 48 seconds, most of it the download. Along the way the script picks the stable channel, verifies the binary’s hash and starts the systemd unit:

[INFO]  Finding release for channel stable
[INFO]  Using v1.36.5+k3s1 as release
[INFO]  Verifying binary download
[INFO]  Installing k3s to /usr/local/bin/k3s
[INFO]  Skipping installation of SELinux RPM
[INFO]  Creating /usr/local/bin/kubectl symlink to k3s
[INFO]  Creating /usr/local/bin/crictl symlink to k3s
[INFO]  Creating /usr/local/bin/ctr symlink to k3s
[INFO]  Creating killall script /usr/local/bin/k3s-killall.sh
[INFO]  Creating uninstall script /usr/local/bin/k3s-uninstall.sh
[INFO]  env: Creating environment file /etc/systemd/system/k3s.service.env
[INFO]  systemd: Creating service file /etc/systemd/system/k3s.service
[INFO]  systemd: Enabling k3s unit
Created symlink '/etc/systemd/system/multi-user.target.wants/k3s.service' → '/etc/systemd/system/k3s.service'.
[INFO]  systemd: Starting k3s

We trimmed the two download lines, which print the GitHub release URLs for the node’s architecture (the arm64 build on a Pi). Check that the service is running:

sudo systemctl status k3s --no-pager | head -10

The unit should be active, and the memory line is worth a glance because it is the first real measure of what a k3s server costs a node:

● k3s.service - Lightweight Kubernetes
     Loaded: loaded (/etc/systemd/system/k3s.service; enabled; preset: enabled)
     Active: active (running) since Mon 2026-10-26 22:22:00 UTC; 17s ago
 Invocation: ab3be80c4c144316949ac9ea92f55d70
       Docs: https://k3s.io
    Process: 26006 ExecStartPre=/sbin/modprobe br_netfilter (code=exited, status=0/SUCCESS)
    Process: 26027 ExecStartPre=/sbin/modprobe overlay (code=exited, status=0/SUCCESS)
   Main PID: 26037 (k3s-server)
      Tasks: 24
     Memory: 579.3M (peak: 579.5M)

Nearly 580 MB seventeen seconds after start, before the bundled Traefik, CoreDNS and metrics-server pods had even settled. This is the number that rules out 2GB modules for server nodes: k3s’s documented minimum for a server is 2 GB, and that minimum leaves nothing for your workloads.

How the other servers join the k3s cluster

On node-b and node-c, set the same two variables. Paste the token you printed on node-a in place of the example value:

export K3S_TOKEN="your-token-here"
export SERVER_IP=10.0.1.11

Then join each one as a server. The --server flag points it at node-a. Cluster-wide settings such as the cluster and service CIDRs and any --disable flags must match on every server, so if you changed those on node-a, repeat them here:

curl -sfL https://get.k3s.io | K3S_TOKEN="${K3S_TOKEN}" sh -s - server --server "https://${SERVER_IP}:6443" --tls-san="${SERVER_IP}" --write-kubeconfig-mode=644

Join them one at a time and wait for each to show Ready before starting the next. Adding two etcd members at once while the first is still syncing is the easiest way to end up with a member that never becomes healthy. Our joins took 43 and 46 seconds. Back on node-a, list the nodes:

kubectl get nodes -o wide

All three should report Ready with both the control-plane and etcd roles, which confirms each one holds an etcd member:

NAME     STATUS   ROLES                AGE     VERSION        INTERNAL-IP   EXTERNAL-IP   OS-IMAGE             KERNEL-VERSION             CONTAINER-RUNTIME
node-a   Ready    control-plane,etcd   2m31s   v1.36.5+k3s1   10.0.1.11     <none>        Ubuntu 26.04.1 LTS   7.0.0-15-generic (amd64)   containerd://2.3.4-k3s1.36
node-b   Ready    control-plane,etcd   76s     v1.36.5+k3s1   10.0.1.12     <none>        Ubuntu 26.04.1 LTS   7.0.0-15-generic (amd64)   containerd://2.3.4-k3s1.36
node-c   Ready    control-plane,etcd   27s     v1.36.5+k3s1   10.0.1.13     <none>        Ubuntu 26.04.1 LTS   7.0.0-15-generic (amd64)   containerd://2.3.4-k3s1.36

On Pi nodes the OS-IMAGE column will read Debian and the kernel column arm64; the roles and versions are what you are checking. To see the cluster carry a real workload, start three nginx replicas and let the scheduler spread them:

kubectl create deployment hello --image=nginx --replicas=3
kubectl rollout status deployment/hello
kubectl get pods -o wide

One replica landed on each node, which is what you want before testing failover:

kubectl get nodes showing three k3s servers with control-plane and etcd roles Ready, and nginx pods spread across nodes

With the workload running, kubectl top nodes reported 709 to 1,088 MiB in use per server on our nodes, with node-a highest because it also runs Traefik, CoreDNS and metrics-server. Plan around that figure when you pick module RAM.

How to run kubectl from your workstation

The kubeconfig k3s writes at /etc/rancher/k3s/k3s.yaml points at https://127.0.0.1:6443, which only works on the node itself. Copy it to your workstation as a separate file, so an existing ~/.kube/config survives, and point it at node-a. Replace pi with the user you created when flashing the nodes:

mkdir -p ~/.kube
scp [email protected]:/etc/rancher/k3s/k3s.yaml ~/.kube/k3s-pi.yaml
sed -i.bak 's/127.0.0.1/10.0.1.11/' ~/.kube/k3s-pi.yaml
export KUBECONFIG=~/.kube/k3s-pi.yaml
kubectl get nodes

We tested the rewritten file against node-a’s LAN address and it listed all three nodes. k3s puts each server’s own addresses in the API certificate automatically, so --tls-san only matters when you reach the cluster through an extra name such as a DNS record or a floating VIP. Every server answers on 6443, so when node-a is down, change the server: line to node-b’s address and keep working. The -i.bak form of sed works with both GNU sed and the BSD sed on macOS. One caveat on --write-kubeconfig-mode=644: it makes this cluster-admin file readable by every user on each node, so drop the flag on nodes other people can log in to. Our kubectl cheat sheet covers the day-to-day commands from here.

Why a LoadBalancer on port 80 stays pending

k3s ships ServiceLB (Klipper), which turns every LoadBalancer Service into a small pod on each node that forwards the Service’s port from the node’s IP. It needs no extra install, which suits a Pi cluster, but it has one rule that catches nearly everyone: two LoadBalancer Services cannot share a port on the same node. Our first attempt exposed nginx on port 80:

kubectl expose deployment hello --port=80 --type=LoadBalancer
kubectl get svc hello

No external IP ever arrived:

NAME    TYPE           CLUSTER-IP      EXTERNAL-IP   PORT(S)        AGE
hello   LoadBalancer   10.43.122.250   <pending>     80:30828/TCP   14s

Waiting longer changes nothing, and the reason sits one layer down in the ServiceLB pods.

EXTERNAL-IP stays <pending> and svclb pods are Pending

The bundled Traefik ingress already holds ports 80 and 443 on every node through its own ServiceLB pods, so the scheduler had nowhere to put the new svclb-hello pods and left all three Pending with no node. Listing every Service shows the owner of the port:

kubectl get svc -A

Traefik’s row at the bottom has claimed 80 and 443 on all three node addresses:

NAMESPACE     NAME             TYPE           CLUSTER-IP      EXTERNAL-IP                       PORT(S)                      AGE
default       hello            LoadBalancer   10.43.122.250   <pending>                         80:30828/TCP                 24s
default       kubernetes       ClusterIP      10.43.0.1       <none>                            443/TCP                      3m46s
kube-system   kube-dns         ClusterIP      10.43.0.10      <none>                            53/UDP,53/TCP,9153/TCP       3m38s
kube-system   metrics-server   ClusterIP      10.43.199.14    <none>                            443/TCP                      3m37s
kube-system   traefik          LoadBalancer   10.43.8.33      10.0.1.11,10.0.1.13,10.0.1.12     80:31191/TCP,443:31867/TCP   2m43s

You have three ways out. Expose the app on a free port, route it through Traefik with an Ingress, or replace ServiceLB with MetalLB so each Service gets its own IP. For the test, a free port is enough:

kubectl delete svc hello
kubectl expose deployment hello --port=8080 --target-port=80 --type=LoadBalancer
kubectl get svc hello

Every node’s address shows up as an external IP this time, and curl -sI http://10.0.1.12:8080 returned HTTP/1.1 200 OK from nginx:

NAME    TYPE           CLUSTER-IP     EXTERNAL-IP                       PORT(S)          AGE
hello   LoadBalancer   10.43.210.63   10.0.1.11,10.0.1.13,10.0.1.12     8080:32291/TCP   14s

For anything beyond a test, the cleaner fix is a real address pool, and our guide to installing MetalLB walks through it. On k3s, MetalLB needs ServiceLB turned off on every server. The K3s networking docs name the flag, --disable=servicelb. On an installed cluster, set it the way K3s’s configuration-file docs map any flag: a disable: key in /etc/rancher/k3s/config.yaml on each server with servicelb as its list item, then sudo systemctl restart k3s. Until MetalLB hands out addresses, Traefik’s own LoadBalancer sits at pending too.

What happens when a Pi node dies

An HA cluster only proves itself when a node goes away, so we killed node-a, the node that founded the cluster and runs the bundled system pods. sudo k3s-killall.sh stops k3s and every container it started, which is close to what a node sees when it loses PoE power. From node-b we polled the nginx Service on port 8080 once a second and listed nodes and pods every ten polls. The API on node-b answered the whole time, because node-b and node-c still held two of three etcd votes.

The Service did not stay clean. In the minute after the kill, 11 of 36 requests timed out, roughly one in three, with the last failure about 55 seconds in, because kube-proxy on node-b still balanced across all three replicas, including the dead one on node-a. Kubernetes only learns a node is gone when the node controller’s grace period runs out. Node-a flipped to NotReady about a minute after the kill, its pod’s Ready condition went false, the endpoint was marked not ready, and from then on every request returned 200:

k3s failover drill: node-a NotReady, its nginx endpoint marked ready=false, and the Service on node-b returning 200

Rescheduling took far longer. The pod on node-a still showed Running for five more minutes, because by default pods tolerate a NotReady node for 300 seconds before eviction. The replacement replica started on node-b about six minutes after the kill. When we ran sudo systemctl start k3s on node-a again, the node came back Ready but the scheduler left two replicas on node-b, since Kubernetes does not rebalance running pods on its own.

That timing is the practical lesson for a Pi cluster. A node that loses PoE power or overheats costs you about a minute of partial failures and several minutes of reduced capacity. Run at least two replicas of anything you care about, set a shorter tolerationSeconds for the node.kubernetes.io/unreachable and not-ready taints on deployments that cannot wait six minutes, and roll a deployment after a failed node returns if you want the replicas spread again. If you outgrow three Pis and want to compare the same cluster on x86, our Kubernetes home cluster build uses mini PCs with the same three-node layout.

What people get wrong about Raspberry Pi clusters

Each of these assumptions shows up again and again in Pi cluster build threads, and each one is cheap to avoid before you order parts:

  • “A CM5 is a drop-in CM4 upgrade.” The connector matches but the boards do not: Turing Pi doesn’t list the CM5 for the 2.5, and DeskPi says the Super6C does not support it. Check the carrier’s own compatibility page before buying modules.
  • “Any PoE HAT works on a Pi 5.” The official PoE+ HAT is listed for the Pi 4 and 3B+. A Pi 5 needs a HAT sold for it, ideally one that carries the NVMe slot too.
  • “etcd is fine on a good microSD card.” K3s’s docs say SD cards and eMMC cannot handle etcd’s IO. Put server nodes on NVMe, or run one server on NVMe and the rest as agents.
  • “More nodes means a faster cluster.” Every server adds an etcd member that must sync each write. Three servers already give you quorum with one failure tolerated, so extra Pis should join as agents.

Still deciding whether a Pi is the right node at all? Our comparison of the Raspberry Pi 5 against an N150 mini PC covers the cost and power trade-off. The Pi 5 kits guide lists the boards worth buying when a cluster is not the goal.

Keep reading

Best UI Applications for Managing Docker Containers Containers Best UI Applications for Managing Docker Containers Install Docker and Run Containers on Ubuntu 24.04|22.04 Containers Install Docker and Run Containers on Ubuntu 24.04|22.04 Install UniFi OS Server on Ubuntu 24.04 LTS Containers Install UniFi OS Server on Ubuntu 24.04 LTS Gatus Monitoring in Docker: Health Checks, Alerts, Status Page Containers Gatus Monitoring in Docker: Health Checks, Alerts, Status Page OpenCloud vs ownCloud vs Nextcloud: Tested on Identical VMs Cloud OpenCloud vs ownCloud vs Nextcloud: Tested on Identical VMs Install Project Quay Registry on OpenShift With Operator Openshift Install Project Quay Registry on OpenShift With Operator

Leave a Comment

Press ESC to close