{"id":16813155,"url":"https://github.com/jmhbnz/raspi-k3s","last_synced_at":"2026-05-16T22:08:14.798Z","repository":{"id":139609466,"uuid":"351921179","full_name":"jmhbnz/raspi-k3s","owner":"jmhbnz","description":"A complete guide for setting up a secure raspberry pi kubernetes cluster using k3s.","archived":false,"fork":false,"pushed_at":"2021-06-26T23:28:22.000Z","size":184,"stargazers_count":0,"open_issues_count":0,"forks_count":2,"subscribers_count":1,"default_branch":"master","last_synced_at":"2026-05-02T17:34:28.505Z","etag":null,"topics":["cluster","k3s","kubernetes","raspberry-pi"],"latest_commit_sha":null,"homepage":"","language":null,"has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/jmhbnz.png","metadata":{"files":{"readme":"readme.org","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2021-03-26T21:55:12.000Z","updated_at":"2021-05-22T20:58:46.000Z","dependencies_parsed_at":"2023-07-16T07:17:15.907Z","dependency_job_id":null,"html_url":"https://github.com/jmhbnz/raspi-k3s","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/jmhbnz/raspi-k3s","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jmhbnz%2Fraspi-k3s","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jmhbnz%2Fraspi-k3s/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jmhbnz%2Fraspi-k3s/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jmhbnz%2Fraspi-k3s/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/jmhbnz","download_url":"https://codeload.github.com/jmhbnz/raspi-k3s/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jmhbnz%2Fraspi-k3s/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33120483,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-16T18:38:32.183Z","status":"ssl_error","status_checked_at":"2026-05-16T18:38:29.903Z","response_time":115,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cluster","k3s","kubernetes","raspberry-pi"],"created_at":"2024-10-13T10:25:00.587Z","updated_at":"2026-05-16T22:08:14.781Z","avatar_url":"https://github.com/jmhbnz.png","language":null,"funding_links":[],"categories":[],"sub_categories":[],"readme":"# -*- ii: ii; -*-\n#+TITLE: Raspberry pi kubernetes cluster guide\n#+AUTHOR: James Blair\n#+EMAIL: mail@jamesblair.net\n#+DATE: 27th March 2021\n\n\nThis file serves as a complete step by step guide for creating a bare metal raspberry pi kubernetes cluster using [[https://k3s.io/][k3s]] from [[https://rancher.com/][Rancher]].\n\nMy goal for this build is to replace a server I currently run at home that hosts several workloads via Docker with a scalable k3s cluster.\n\nAdditionally in future I would like the cluster to be portable and operate via 3G-5G Cellular network and an array of batteries.\n\nI chose k3s as it incredibly lightweight but still CNCF certified and production grade software that is optimised for resource constraints of raspberry pis.\n\n\n* Hardware pre-requisites\n\n** Cluster machines\n\nFor this guide I am using four [[https://www.pishop.us/product/raspberry-pi-4-model-b-4gb/][Raspberry Pi 4 4GB]] machines and one [[https://www.pbtech.co.nz/product/SEVRBP0267/Raspberry-Pi-4-Model-B-8GB-LPDDR4-Quad-Core-Cortex][Raspberry Pi 4 8GB]] for more memory hungry workloads.\n\n\n** Boot media\n\nThis guide requires each Raspberry Pi to have a removable SD card or other removable boot media.  I am using five 32GB SD Cards though any USB or SD card at least 8GB in size should work fine.\n\n*Note:* Newer raspberry pi firmware can support USB or even NVME booting. If boot disk IO performance is the goal of your cluster you may want to explore this instead. Additionally network booting is also possible if you would prefer to avoid using SD Cards or other attached boot media.\n\n\n** Power supply\n\nFor this cluster I am using power over ethernet using [[https://www.pbtech.co.nz/product/SEVRBP0184/Raspberry-Pi-Add-On-Board-Power-over-Ethernet-PoE][Pi POE Hat]] addon boards. This means each Pi does not need a separate cable for power supply so is much cleaner.\n\n*Note:* A power over ethernet cable switch is required for this configration. I am using [[https://www.pbtech.co.nz/product/SWHNGR1208120/NETGEAR-ProSAFE-GS108PP-8-Port-Gigabit-Unmanaged-P][this 8 port gigabit Netgear switch]] which supports 120 watts for POE.  Each Pi will require at least 15 watts.\n\n\n* Step 1 - Prepare boot media for master \n\n** Download the latest release\n\nOur first step is to create the bootable SD Card with a minimal install of [[https://www.raspbian.org/][Raspbian]], which is a free operating system based on [[https://www.debian.org/][Debian]] and is optimised for Raspberry Pi hardware.\n\nRather than doing an installation and configuration of an os image from scratch I found [[https://github.com/FooDeas/raspberrypi-ua-netinst][this project]] on Github which automates the install and configuration process nicely.\n\n#+NAME: Download the latest release zip\n#+begin_src tmate\ncd ~/Documents/raspi-k3s\necho Downloading latest release zip from github\ncurl -s https://api.github.com/repos/foodeas/raspberrypi-ua-netinst/releases/latest \\\n  | grep \"browser_download_url.*zip\" \\\n  | cut -d : -f 2,3 \\\n  | tr -d \\\" \\\n  | wget -i -\n  \necho Checking file is now present\nls -l | grep *.zip\n  \necho Extracting the zip file\nunzip -q -d installer *.zip\nls -l | grep installer\n#+end_src\n\n#+RESULTS: Download the latest release zip\n#+begin_example\nDownloading latest release zip from github\nChecking file is now present\n-rw-rw-rw- 1 james james 60299545 Aug 12 08:35 raspberrypi-ua-netinst-v2.4.0.zip\nExtracting the zip file\ndrwxrwxrwx 1 james james     4096 Jan 20 11:12 installer\n-rwxrwxrwx 1 james james     2863 Jan 10 17:04 installer-config.txt\n#+end_example\n\n\n** Apply custom install configuration\n\nOur next step after downloading the latest release is to apply our own installation configuration using a simple txt file.\n\nThere is great documentation online showing what configuration options are available [[https://github.com/malignus/raspberrypi-ua-netinst/blob/master/doc/INSTALL_CUSTOM.md][here]].\n\nFor our purposes we just over-write the file downloaded and extracted in the previous step with one we have prepared earlier :)\n\n#+NAME: Overwrite installer configuration file\n#+begin_src tmate\necho Display wordcount of file after copy to validate update wordcount of original file for comparison\nwc installer/raspberrypi-ua-netinst/config/installer-config.txt\n\necho Overwriting /installer/raspberrypi-ua-netinst/config/installer-config.txt\ncp installer-config.txt installer/raspberrypi-ua-netinst/config/\n\necho Display wordcount of file after copy to validate update\nwc installer/raspberrypi-ua-netinst/config/installer-config.txt\n#+end_src\n\n#+RESULTS: Overwrite installer configuration file\n#+begin_example\nDisplay wordcount of original file for comparison\n3  23 157 installer/raspberrypi-ua-netinst/config/installer-config.txt\nOverwriting /installer/raspberrypi-ua-netinst/config/installer-config.txt\nDisplay wordcount of file after copy to validate update\n67   85 2863 installer/raspberrypi-ua-netinst/config/installer-config.txt\n#+end_example\n\n\n** Apply custom post install script\n\nThe final step is to supply a post install script which completes additional security hardening and production readiness automatically.\n\nTo supply a script we can provide an additional ~post-install.txt~ file as documented [[https://github.com/FooDeas/raspberrypi-ua-netinst/blob/devel/doc/INSTALL_ADVANCED.md][here]].\n\nI have a hardening script prepared in this repository that we can copy in.\n\n#+NAME: Copy in post-install script\n#+begin_src tmate\necho Copying in post-install.txt\ncp post-install.txt installer/raspberrypi-ua-netinst/config/\n\necho Display wordcount of file after copy to validate\nwc installer/raspberrypi-ua-netinst/config/post-install.txt\n#+end_src\n\n#+RESULTS: Copy in post-install script\n#+begin_example\nCopying in post-install.txt\nDisplay wordcount of file after copy to validate\n98  282 3429 installer/raspberrypi-ua-netinst/config/post-install.txt\n#+end_example\n\n\n* Step 2 - Copy the install media to sd card\n\nOur next step is to copy the contents of the ~installer/~ folder to a *FAT32* formatted removable media i.e. SD Card.\n\nUnfortunately this is currently a windows step as my dev environment is a Windows 10 laptop with Debian via Windows Subsystem for Linux which does not support ~lsblk~ or other disk management commands.\n\n** Obtain sd card partition information\n\nOur first step is to insert the SD Card and ensure it is formatted correctly as ~FAT32~.  To do that we need to know the number of the disk we want to format, we can find that via powershell.\n\n#+NAME: Get disks via windows powershell\n#+begin_src tmate\necho Retrieving disk list via powershell\npowershell.exe -nologo -command \"get-disk | select Number, FriendlyName, Size\"\n#+end_src\n\n#+NAME: Get partitions via windows powershell\n#+begin_src tmate\necho Retrieving partition list via powershell\npowershell.exe -nologo -command \"get-disk | get-partition | select PartitionNumber, DriveLetter, Size, Type\"\n#+end_src\n\n\n** Create and format sd card partition\n\nOnce we know the number of the disk we want to format we can proceed. In the example above I have a 32GB SD Card which shows as number ~1~.\n\nChecking the disk we can see some partitions that exist already from previous use of the card.  To delete these partitions you can use the ~Remove-Partition -DiskNumber X -PartitionNumber Y~ command where ~X~ and ~Y~ relate to the output of your disk and partition number.\n\nDue to the risk of data loss this step is not automated. Once existing partitions have been cleared we can use the following block to:\n  - Create a new partition using maximum available space\n  - Assign a free drive letter in windows\n  - Mount the disk in WSL so we can copy to it\n  - Copy the install media over to the partition \n\n#+NAME: Create sd card partition\n#+begin_src tmate\necho Use powershell to create new partition and format\npowershell.exe -nologo -command \"new-partition -disknumber 1 -usemaximumsize -driveletter d; format-volume -driveletter d -filesystem FAT32 -newfilesystemlabel sd\"\n#+end_src\n\n#+NAME: Mount and copy the new media\n#+begin_src tmate\necho Mount the new partition in wsl\nsudo mkdir /mnt/d\nsudo mount -t drvfs d: /mnt/d/\n\necho Copy the contents of installer to sd\ncp -r installer/* /mnt/d/\n  \n# We need to wait before we can eject\nsleep 5\nsudo umount /mnt/d\n\nsleep 5\necho Eject the sd card ready for use\npowershell.exe -nologo -command \"(new-object -comobject shell.application).namespace(17).parsename('D:').invokeverb('eject')\"\n#+end_src\n\n\n* Step 3 - Boot the pi and remotely connect\n\nProvided the configuration on the sd card is valid and the pi has been able to successfully obtain an ip address via dhcp on boot then following a 10-20minute net install process the pi will be online and accessible via ssh using the private key corresponding to the public key we supplied in our ~installer-config.txt~ file.\n  \n** Port knock and enter\n\nNow we can port knock and connect.\n\nNote: There seems to be a tiny delay required between port knocks being transmitted and ssh being able to connect which is why a short sleep is included in the knock and enter command.\n\n#+NAME: Knock and enter\n#+begin_src tmate\n# Setup machine variables\nexport port=2124\nexport machineip=192.168.1.124\nexport knocksequence=\"[SEQUENCE HERE]\"\n  \n# Gather ssh keys if not already known\nssh-keyscan -p $port $machineip \u003e\u003e ~/.ssh/known_hosts\n \n# Knock and enter\nknock $machineip $knocksequence \u0026\u0026 sleep 2 \u0026\u0026 ssh -p $port $machineip\n#+end_src\n\n\n* Step 4 - Configure distributed storage\n\nOne of the goals for this raspberry pi cluster is to run with distributed storage, rather than a traditional single device raid array that the server this cluster is replacing is currently running.\n\nThe reason I'm interested in this is primarily to explore options for greater hardware redunancy and reliability in the event that a node may go down within the cluster.\n\n** Format and mount storage volumes\n\nNow that our machines are online and we have connected we can setup our storage cluster.\n\nFor a distributed storage cluster we are using [[https://www.gluster.org/][glusterfs]]. As part of our earlier setup gluster was automatically installed.  We just need to configure it.\n\nOur first step is to ensure our storage drives attached to our raspberry pi's are formatted. In our case our drives are all showing as ~/dev/sda~ with no existing partitions, ensure you review your situation with ~lsblk~ first and ajdust the commands below as neccessary!\n\n#+NAME: Format and mount storage bricks\n#+begin_src tmate\n# Format the /dev/sda1 partition as xfs\nsudo mkfs.xfs -i size=512 /dev/sda1\n  \n# Make the mount point directory\nsudo mkdir -p /data/brick1\n\n# Update fstab to ensure the mount will resume on boot\necho '/dev/sda1 /data/brick1 xfs defaults 1 2' | sudo tee -a /etc/fstab\n  \n# Mount the new filesystem now\nsudo mount -a \u0026\u0026 sudo mount\n#+end_src\n\n\n** Configure firewall rules\n\nThe gluster processes on the nodes need to be able to communicate with each other. To simplify this setup, configure the [[https://en.wikipedia.org/wiki/Iptables][iptables]] firewall on each node to accept all traffic from the other node(s).\n\nIn our four node cluster this means ensuring we have rules present for all nodes. Adjust as neccessary for the requirements of your cluster!\n\n#+NAME: Setup firewall rules for inter cluster communication\n#+begin_src tmate\n# Add the firewall rules\nsudo iptables -I INPUT -p all -s 192.168.1.122 -j ACCEPT\nsudo iptables -I INPUT -p all -s 192.168.1.124 -j ACCEPT\nsudo iptables -I INPUT -p all -s 192.168.1.126 -j ACCEPT\nsudo iptables -I INPUT -p all -s 192.168.1.128 -j ACCEPT\nsudo iptables -I INPUT -p all -s 192.168.1.130 -j ACCEPT\n  \n# Ensure these are saved permanently\nsudo netfilter-persistent save\n#+end_src\n\n\n** Ensure the daemon is running\n\nNext we need to ensure the glusterfs daemon is enabled and started.\n\n#+NAME: Ensure glusterd is enabled and running\n#+begin_src tmate\n# Ensure the gluster service starts on boot\nsudo systemctl enable glusterd\n\n# Start the gluster service now\nsudo systemctl start glusterd\n\n# Check the service status to confirm running\nsudo systemctl status glusterd\n#+end_src\n\n\n** Test connectivity between peers\n\nNow we're ready to test connectivity between all the gluster peers.\n  \n#+NAME: Complete cluster probes\n#+begin_src tmate\n# Complete the peer probes\nsudo gluster peer probe 192.168.1.122\nsudo gluster peer probe 192.168.1.124\nsudo gluster peer probe 192.168.1.126\nsudo gluster peer probe 192.168.1.128\nsudo gluster peer probe 192.168.1.130\n\n# Validate the peer status\nsudo gluster peer status\n#+end_src\n\n\n** Setup gluster volume\n\nProvided connectivity was established successfully you are now ready to setup a gluster volume.\n\n*Note:* The ~gluster volume create~ command only needs to be run from any one node.\n\n#+NAME: Setup gluster volume\n#+begin_src shell :wrap example\n# Create the gluster volume folder (all nodes)\nsudo mkdir -p /data/brick1/jammaraid\n\n# Create the gluster volume itself (one node)\nsudo gluster volume create jammaraid 192.168.1.122:/data/brick1/jammaraid 192.168.1.124:/data/brick1/jammaraid 192.168.1.126:/data/brick1/jammaraid 192.168.1.128:/data/brick1/jammaraid force\n\n# Ensure the volume is started\nsudo gluster volume start jammaraid\n\n# Confirm the volume has been created\nsudo gluster volume info\n#+end_src\n\n\n** Mount and use the new volume\n\nNow that the gluster volume has been created and started we can mount it within each node so it is accessible for use :)\n\n#+NAME: Mount the gluster volume\n#+begin_src tmate\n# Create the gluster volume mount point\nsudo mkdir -p /media/raid\n \n# Mount the volume\nsudo mount -t glusterfs localhost:jammaraid /media/raid\n#+end_src\n\n\n* Step 5 - Create kubernetes cluster\n\nNow can begin installing [[http://k3s.io/][k3s]] on each of the cluster nodes, and then join them into one compute cluster. This will set us up to be able to deploy workloads to that kubernetes cluster.\n\n** Download k3s setup binary\n\nOur first step is to download the latest ~k3s-armhf~ setup binary from github. Repeat the steps below for each potential cluster node.\n\n#+NAME: Knock and enter\n#+begin_src tmate\n# Setup machine variables\nexport port=2128\nexport machineip=192.168.1.128 \nexport knocksequence=\"[SEQUENCE HERE]\"\n\n# Gather ssh keys if not already known\nssh-keyscan -p $port $machineip \u003e\u003e ~/.ssh/known_hosts\n\n# Knock and enter\nknock $machineip $knocksequence \u0026\u0026 sleep 2 \u0026\u0026 ssh -p $port $machineip \n#+end_src\n\n#+NAME: Download latest setup binary\n#+begin_src tmate :wrap example\n# Download the latest release dynamically\ncurl -s https://api.github.com/repos/rancher/k3s/releases/latest \\\n      | grep \"browser_download_url.*k3s-armhf\" \\\n      | cut -d : -f 2,3 \\\n      | tr -d \\\" \\\n      | wget -i -\n\n# Make it executable\nchmod +x k3s-armhf\n\n# Leave the node\nexit\n#+end_src\n\n\n** Initialise the cluster\n\nOur next step we only run on the one node that will operate as our cluster master. K3s provides an installation script that is a convenient way to install it as a service on systemd or openrc based systems. This script is available at https://get.k3s.io. \n\nAfter running this installation:\n\n * The ~k3s~ service will be configured to automatically restart after node reboots or if the process crashes or is killed.\n * Additional utilities will be installed, including ~kubectl~, ~crictl~, ~ctr~, ~k3s-killall.sh~, and ~k3s-uninstall.sh~.\n * A ~kubeconfig~ file will be written to ~/etc/rancher/k3s/k3s.yaml~ and the kubectl installed by K3s will automatically use it.\n\nFirst step, let's login to our chosen master.\n\n#+NAME: Knock and enter\n#+begin_src tmate\n# Setup machine variables\nexport port=2124\nexport machineip=192.168.1.124\nexport knocksequence=\"[SEQUENCE HERE]\"\n\n# Gather ssh keys if not already known\nssh-keyscan -p $port $machineip \u003e\u003e ~/.ssh/known_hosts\n\n# Knock and enter\nknock $machineip $knocksequence \u0026\u0026 sleep 2 \u0026\u0026 ssh -p $port $machineip \n#+end_src\n\n\nOnce we have logged in we can run the install script.\n\n#+NAME: Initialise the master node\n#+begin_src tmate\ncurl -sfL https://get.k3s.io | sh -\n#+end_src\n\n\nOnce our master has been deployed by the installation script we can check ~kubectl~ to ensure they are listed as expected.\n\n#+NAME: Check cluster nodes\n#+begin_src tmate\n# Check kubectl\nsudo kubectl get nodes\n\n# Obtain cluster token\nsudo cat /var/lib/rancher/k3s/server/node-token\n#+end_src\n\n\n** Join worker nodes\n\nOnce we have established our cluster masters we need to join workers into the cluster. To install on worker nodes and add them to the cluster, run the installation script with the K3S_URL and K3S_TOKEN environment variables. \n\nRepeat the steps below for each worker node, ensuring the node port, machineip and knocksequence are set correctly. \n\n#+NAME: Knock and enter\n#+begin_src tmate\n# Setup machine variables\nexport port=2128\nexport machineip=192.168.1.128\nexport knocksequence=\"[SEQUENCE HERE]\"\n\n# Gather ssh keys if not already known\nssh-keyscan -p $port $machineip \u003e\u003e ~/.ssh/known_hosts\n\n# Knock and enter\nknock $machineip $knocksequence \u0026\u0026 sleep 2 \u0026\u0026 ssh -p $port $machineip \n#+end_src\n\n#+NAME: Join worker\n#+begin_src tmate\n# Set environment variables\nexport K3S_URL=https://192.168.1.124:6443\nexport K3S_TOKEN=[TOKEN_HERE]\n\n# Run the installation script\ncurl -sfL https://get.k3s.io | sh -\n\n# Leave the worker\nexit\n#+end_src\n\n\n** Check the cluster status\n\nOnce all workers have been joined lets hop back onto the master and confirm that all nodes are listed as expected.\n\n#+NAME: Knock and enter\n#+begin_src tmate\n# Setup machine variables\nexport port=2124\nexport machineip=192.168.1.124\nexport knocksequence=\"[SEQUENCE HERE]\"\n\n# Gather ssh keys if not already known\nssh-keyscan -p $port $machineip \u003e\u003e ~/.ssh/known_hosts\n\n# Knock and enter\nknock $machineip $knocksequence \u0026\u0026 sleep 2 \u0026\u0026 ssh -p $port $machineip \n#+end_src\n\n\n#+NAME: Check cluster nodes\n#+begin_src tmate\n# Check kubectl\nsudo kubectl get nodes\n#+end_src\n\n\n* Step 6 - Deploy a service\n\nWith our cluster now running, now we can take it for a spin! Let's deploy a simple service. We'll deploy figlet which will take a body over HTTP on port 8080 and return an ASCII-formatted string.\n\nWe'll need to be logged into our cluster master to do this.\n\n#+NAME: Create the service\n#+begin_src tmate\ncat \u003c\u003cEOF \u003e openfaas-figlet-svc.yaml\napiVersion: v1\nkind: Service\nmetadata:\n  name: openfaas-figlet\n  labels:\n    app: openfaas-figlet\nspec:\n  type: NodePort\n  ports:\n    - port: 8080\n      protocol: TCP\n      targetPort: 8080\n      nodePort: 31111\n  selector:\n    app: openfaas-figlet\nEOF\n#+end_src\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjmhbnz%2Fraspi-k3s","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjmhbnz%2Fraspi-k3s","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjmhbnz%2Fraspi-k3s/lists"}