preloader
blog-post

How to set up Kubernetes cluster with Raspberry Pi

Table of Contents

In this article, we will see how to install and configure a Kubernetes cluster on a Raspberry Pi. Docker, Ansible, k3s, and the Raspberry Pi 4 (8GB) were used for this setup. K3s is a fully compliant, lightweight Kubernetes distribution.

Hardware List

List of required hardware components:

Hardware Type Model
Raspberry Pi Raspberry Pi 4 (8 GB)
Case C4Labs Cloudlet Cluster Case
Memory Card SanDisk 128GB Ultra MicroSDXC
Power Supply Anker PowerPort 6 (60W 6-Port USB Charging Hub)
USB Cable 1 Feet Cabepow USB A to Type C Cable
Cat6 Cable Rankie RJ45 Cat6
Wireless Router TP-Link Wireless N Nano Router
Network Switch TP-Link 5 Port Gigabit Ethernet Network Switch
The wireless router is optional, used only to make the cluster portable — the switch’s uplink port connects to it in Client mode, bridging the cluster onto your existing Wi-Fi network. The wireless router and network switch used here are outdated models because I used what I already had available; if buying new, go for an 8-port switch and the latest Wireless N Nano Router.

Hardware setup

  • Unbox the Raspberry Pi and all accessories

  • Attach the fan to the case

  • Install heat sinks on the CPU, RAM, and USB controller chip

  • Install the fan by connecting the RED wire to the second pin and the BLACK wire to the third pin in the top row from the left side

  • Safely attach the Raspberry Pi to the case and connect the power supply and ethernet cables

  • Attach the network switch, router, and power supply to the case using double-sided tape (optional). If you are not using a wireless router, you can also connect the Pis directly to the router using Cat 6 cables

Install OS

  • Download and install Etcher on your machine

  • Download the Ubuntu image for Raspberry Pi. Pick the OS type you need. We will use Ubuntu Server for this setup, but you can also use Ubuntu Desktop if you have a micro HDMI cable and a USB keyboard

  • Insert the SD card into your machine and flash it using Etcher to install Ubuntu for Raspberry Pi

  • For Ubuntu Server, enable SSH by adding an empty file named ssh inside the “system-boot” directory of the SD card. This lets us bring up the Raspberry Pi completely headless

  • For Ubuntu Desktop, connect the Pi to a display and keyboard, then enable SSH using the following method

    • Update all packages
    sudo apt update
    sudo apt upgrade
    
    • Install OpenSSH server
    sudo apt install openssh-server
    
    • Enable the SSH service
    sudo service ssh enable
    sudo service ssh start
    
  • Insert the SD card into each Raspberry Pi’s SD slot and power on the devices

  • Identify the IP address of all Raspberry Pis using the nmap command, or from the router’s admin console. It’s advisable to reserve the IP address for each Raspberry Pi in the router settings so the IP addresses won’t change with future restarts

    # Adjust the IP address to the default gateway address of your network
    sudo nmap -sn 192.168.0.1/24
    
    If your network includes a Wi-Fi extender or mesh/nano router (e.g. a travel router bridging the cluster’s switch to your main network), check that its own DHCP server is disabled. A second active DHCP server on the same network will race with your router’s reservations, causing IP addresses to shuffle unpredictably between devices on every reboot even after reservations are configured correctly.
  • SSH into each Raspberry Pi using the credential ubuntu/ubuntu and change the default credential to a more secure one

    If you’re resuming a cluster that’s been powered off for an extended period, expect the OS to be far behind on updates and the k3s TLS certificates to have expired. Run a full apt update && apt upgrade and plan on reinstalling k3s (see Install Kubernetes) rather than trying to revive the existing installation.

Install k3s

We will use the official Ansible template from k3s, plus a few additional Ansible playbooks, to install and configure k3s.

🔗 Ansible Playbook - k3s 🔗 Ansible Playbook - Pi Config

k3s-ansible has restructured since this post was first written — it’s now an Ansible collection using inventory.yml with server/agent groups, instead of the old hosts.ini with master/node. The steps below use the current structure and have been tested end-to-end against a clean cluster.

Clone both repositories side by side into a single parent directory, and keep a $REPOS variable pointing at it — every cd command in the rest of this post uses it, so re-export it at the start of each new kube-tools shell:

export REPOS=/path/to/where/you/keep/repos   # must be on the host mount kube-tools bind-mounts (check its docker-compose.yml volumes) - not the container's own home directory, which is wiped if kube-tools is ever recreated
mkdir -p $REPOS && cd $REPOS
git clone https://github.com/k3s-io/k3s-ansible
git clone https://github.com/entechlog/kubernetes-examples

Prerequisite

Download and install Docker for your platform. Click here for instructions

Start Docker container

  • Open a new terminal and cd into the kubernetes-examples/kube-tools directory. This directory contains a Dockerfile and docker-compose.yml to bring up a Docker container with Ansible.

    The kube-tools image was originally based on python:3.9-slim-buster. Debian Buster’s LTS support ended June 2024 and its apt repositories have been taken offline, so the image will fail to build (apt-get update returns 404s). It’s been updated to python:3.13-slim-trixie (Debian 13, the current stable release — note Debian 12 “Bookworm” moved to LTS-only maintenance in July 2026, so it’s still usable but no longer current). The Dockerfile also needed fixes for a few other repos/URLs that have since changed:

    • The Kubernetes apt repo moved from apt.kubernetes.io to pkgs.k8s.io (the legacy repo was fully shut down March 2024) — also bump the pinned version path (core:/stable:/v1.30/) to whatever the current maintained minor version is; Kubernetes only maintains the latest 3 minor releases.
    • Helm’s apt-key-based install should be replaced with its official install script (https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3).
    • The k9s release asset naming changed (k9s_Linux_x86_64.tar.gzk9s_Linux_amd64.tar.gz) somewhere between versions, and the octant install step hardcoded its .deb filename instead of using ${OCTANT_VERSION}, silently breaking if that version is ever bumped. Also worth knowing: Octant itself was archived by VMware in January 2023 and is no longer maintained — the repo lives on as a read-only vmware-archive/octant.
  • Create a copy of .env.template as .env

    No changes needed in .env
  • Start the kube-tools container by running

    docker compose up -d
    
  • Validate the container by running

    docker ps
    
  • Shell into the container by running

    docker exec -it kube-tools /bin/bash
    
  • Validate Ansible by running

    ansible --version
    

Prepare playbooks

  • cd into the cloned Pi-config repository

    cd $REPOS/kubernetes-examples/rpi-k3/configure/
    
  • Edit hosts.ini with the IP addresses gathered above, and a user-friendly hostname you’d like to set for each Raspberry Pi, in the format below.

    [master]
    pi-kube-m1 ansible_ssh_host=192.168.0.100
    
    [node]
    pi-kube-n1 ansible_ssh_host=192.168.0.101
    pi-kube-n2 ansible_ssh_host=192.168.0.102
    
    [k3s_cluster:children]
    master
    node
    
  • cd into the cloned k3s-ansible repository

    cd $REPOS/k3s-ansible/
    
  • Install the collection’s Ansible dependencies

    ansible-galaxy collection install -r collections/requirements.yml
    
    If you’re running this inside the kube-tools container on Windows, Ansible silently ignores this repo’s own ansible.cfg (which sets roles_path) because the bind-mounted host drive looks “world writable” to Linux — you’ll hit the role 'prereq' was not found. Work around it by setting the roles path explicitly before running any playbook here: export ANSIBLE_ROLES_PATH=$(pwd)/roles
  • Copy the sample inventory to inventory.yml

    cp inventory-sample.yml inventory.yml
    
  • Edit inventory.yml with the IP addresses gathered above, in the following format. Generate a value for token with openssl rand -base64 64 — this is the shared secret nodes use to join the cluster, so don’t reuse the example below, and don’t commit it anywhere public.

    k3s_cluster:
      children:
        server:
          hosts:
            192.168.0.100:
        agent:
          hosts:
            192.168.0.101:
            192.168.0.102:
      vars:
        ansible_user: ubuntu
        k3s_version: v1.36.3+k3s1
        token: "<output of openssl rand -base64 64>"
        api_endpoint: "{{ hostvars[groups['server'][0]]['ansible_host'] | default(groups['server'][0]) }}"
    

    Check the k3s releases page for a current stable version to use instead of the one shown above.

Generate RSA key and set up passwordless login

  • cd into the cloned Pi-config repository

    cd $REPOS/kubernetes-examples/
    
  • Generate an RSA key by running the following playbook

    ansible-playbook rpi-k3/configure/01-generate-rsa.yml
    

  • ⚠️ Alternatively, the RSA key can be generated manually by running the following commands ⚠️

    ssh-keygen -t rsa
    cat /home/ubuntu/.ssh/id_rsa.pub > ~/.ssh/authorized_keys
    
  • Copy the RSA key to all Raspberry Pis by running the following playbook

    export ANSIBLE_HOST_KEY_CHECKING=False
    ansible-playbook rpi-k3/configure/02-copy-rsa.yml -i rpi-k3/configure/hosts.ini --ask-pass
    export ANSIBLE_HOST_KEY_CHECKING=True
    

  • ⚠️ Alternatively, the RSA key can be copied manually by running the following command for each Pi ⚠️

    cat /home/ubuntu/.ssh/id_rsa.pub | ssh ubuntu@192.168.0.100 'mkdir -p ~/.ssh && chmod 700 ~/.ssh && cat >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys'
    

Set hostname

  • Update the hostnames of the Raspberry Pis by running the playbook below

    ansible-playbook -e 'reboot=True' \
    rpi-k3/configure/03-change-hostname.yml -i rpi-k3/configure/hosts.ini
    

  • ⚠️ Alternatively, the hostname can also be changed manually by SSHing into each Pi and editing the hostname in /etc/hostname and /etc/hosts, then issuing a reboot command ⚠️

If you get a “/bin/sh: 1: /usr/bin/python3: not found” error, run the command below to install Python: ansible-playbook rpi-k3/configure/99-install-python.yml -i rpi-k3/configure/hosts.ini

Fix cgroup v2

Kubernetes v1.35+ requires cgroup v2 (FailCgroupV1 defaults to true) and the kubelet refuses to start without it, but k3s-ansible’s Raspberry Pi role still only enables the legacy cgroup v1 flags as of this writing. Rather than hand-patching files inside the cloned collection (fragile, and easy to lose track of on a re-clone), this is a small standalone playbook that only touches the boot commandline.
  • cd into the cloned Pi-config repository

    cd $REPOS/kubernetes-examples/
    
  • Run the following playbook to enable cgroup v2 and reboot the Pis

    ansible-playbook rpi-k3/configure/06-fix-cgroup-v2.yml -i rpi-k3/configure/hosts.ini
    

Install Kubernetes

  • cd into the cloned k3s-ansible repository

    cd $REPOS/k3s-ansible/
    
  • Every ansible-playbook command from here needs a private key, disabled host-key checking, and (per the warning above) an explicit roles path — set all three once per shell session:

    export ANSIBLE_PRIVATE_KEY_FILE=/path/to/your/id_rsa
    export ANSIBLE_HOST_KEY_CHECKING=False
    export ANSIBLE_ROLES_PATH=$(pwd)/roles
    
  • Start provisioning the cluster using the following command

    ansible-playbook playbooks/site.yml -i inventory.yml
    
  • Get the kubeconfig from the master and set KUBECONFIG. Save it under kubernetes-examples/kube-tools/.kube/ on the mounted drive, not ~/.kube/config — the latter is container-local and gets wiped if kube-tools is ever recreated:

    mkdir -p $REPOS/kubernetes-examples/kube-tools/.kube
    scp ubuntu@192.168.0.100:~/.kube/config $REPOS/kubernetes-examples/kube-tools/.kube/config
    export KUBECONFIG=$REPOS/kubernetes-examples/kube-tools/.kube/config
    

    Unlike the older role, the current collection doesn’t rewrite the kubeconfig’s server address for you — it’ll point at 127.0.0.1, which only works from the master itself. Fix it before using kubectl from your own machine:

    kubectl config set-cluster default --server=https://192.168.0.100:6443 --kubeconfig $KUBECONFIG
    
  • Validate the Kubernetes nodes by running

    kubectl get nodes
    

    All nodes showing Ready doesn’t guarantee the pod network between them actually works. k3s-agent nodes can come up with an incomplete Flannel mesh — each agent only learns the server’s pod route during a race at initial join, not the other agents’ — so cross-node pod traffic (e.g. a pod on node A reaching a pod on node B) can silently time out while the install itself reports success. Run this once, back in kubernetes-examples/ (not k3s-ansible/), to force Flannel to redo peer discovery and confirm the mesh is actually complete:

    cd $REPOS/kubernetes-examples/
    ansible-playbook rpi-k3/configure/07-verify-cluster-network.yml -i rpi-k3/configure/hosts.ini
    

    It fails loudly if any node is still missing routes, rather than leaving you to debug a mysterious connection timeout later (this is exactly how the issue first showed up — as a Grafana panel unable to reach Prometheus).

  • If you want to reset the Kubernetes cluster, you can do so by running the following command first, then starting over from Install Kubernetes

    ansible-playbook playbooks/reset.yml -i inventory.yml
    

Monitor the cluster

A cluster with nothing running on it doesn’t prove much, so let’s put it to real use: monitoring itself. kube-prometheus-stack bundles Prometheus, Grafana, Alertmanager, node-exporter, and kube-state-metrics into one Helm chart, giving you node and pod health over time rather than just a point-in-time snapshot.

The chart’s defaults assume 2-4Gi of memory for Prometheus alone, which doesn’t fit alongside k3s on Pi-class hardware. rpi-k3/monitoring/values.yaml trims requests/limits down to a footprint that fits comfortably on a 3-node Raspberry Pi 4 (8GB) cluster, and extends Grafana’s startup probes — first boot on a Pi is slow, since Grafana downloads a handful of default app plugins over the network before it starts serving, and the chart’s default probe timing kills the pod before that finishes.
  • Install the monitoring stack — inside kube-tools, with KUBECONFIG still exported from the previous section:

    cd $REPOS/kubernetes-examples/
    bash rpi-k3/monitoring/install-monitoring.sh
    

    This adds the Helm repo, installs the chart with the trimmed values.yaml, and waits for all pods to be ready. First install can take several minutes (image pulls + Grafana’s plugin downloads); later runs are much faster since images are cached.

    Run the script this way (already cd’d into place inside a kube-tools shell), not as docker exec kube-tools bash rpi-k3/monitoring/install-monitoring.sh from your host terminal — docker exec resolves relative paths against the container’s own default directory, not wherever your host shell happens to be, so a bare relative path there fails with “No such file or directory”.
  • Open Grafana — run this on your own machine, not inside a container, cd’d into the same kubernetes-examples/ directory (on the host path, not $REPOS — that variable only exists inside the kube-tools shell where you exported it)

    bash rpi-k3/monitoring/open-monitoring.sh
    

    This fetches the Grafana admin password, opens an SSH tunnel, and launches your browser at http://localhost:3000 once the tunnel is up. Log in as admin with the printed password.

See <code>rpi-k3/monitoring/README.md</code> for more detail, including a known kubectl top/metrics-server issue on this setup that’s unrelated to the monitoring stack itself (Prometheus scrapes node-exporter and kube-state-metrics directly, independent of the Kubernetes metrics-server API).

Optional: Kubernetes Dashboard

Kubernetes Dashboard is a general-purpose, web-based UI for browsing and editing cluster resources — useful for ad-hoc debugging, but not something you need running continuously alongside the monitoring stack above, and its multi-pod footprint (api, auth, web, metrics-scraper, and a Kong gateway) is worth the reserved capacity only if you’ll actually use it. See <code>rpi-k3/dashboard/README.md</code> for install/access instructions using the same install/open script pattern as monitoring.

Now we have a Kubernetes cluster running on Raspberry Pis, actively monitoring itself and ready to run more workloads.

Clean Demo Resources

Inside kube-tools (docker exec -it kube-tools /bin/bash), with KUBECONFIG exported as in Install Kubernetes (re-export it if this is a fresh shell). Run the command below to remove the monitoring stack

helm uninstall kube-prometheus-stack -n monitoring
kubectl delete namespace monitoring

If you also installed the optional dashboard, see <code>rpi-k3/dashboard/README.md</code> for its removal steps.

Reset the Pis completely

If you want to wipe k3s off the Raspberry Pis entirely — not just the workloads on top — cd into the cloned k3s-ansible repository and run the reset playbook against all three nodes. This is the same command used to test this post’s instructions from a clean state, so it’s verified to actually leave the Pis ready for a fresh Install Kubernetes run.

If this is a fresh kube-tools shell, re-export the same variables from Install Kubernetes first — without them the reset playbook fails the same way the install one does (Permission denied or the role 'prereq' was not found):

export REPOS=/path/to/where/you/keep/repos
cd $REPOS/k3s-ansible/
export ANSIBLE_PRIVATE_KEY_FILE=/path/to/your/id_rsa
export ANSIBLE_HOST_KEY_CHECKING=False
export ANSIBLE_ROLES_PATH=$(pwd)/roles
ansible-playbook playbooks/reset.yml -i inventory.yml

This removes the k3s binaries, systemd services, and all cluster data (/etc/rancher/k3s, /var/lib/rancher/k3s, /var/lib/kubelet) from every node — the Pis themselves, hostnames, and SSH access are untouched.

Hope this was helpful. Did I miss something? Let me know in the comments.

References

Share this blog:
Comments

Related Articles