Skip to content

Instantly share code, notes, and snippets.

@djmeph
Last active March 24, 2025 03:50
Show Gist options
  • Save djmeph/7ebae543af52a87191e6b7fe03c8d16b to your computer and use it in GitHub Desktop.
Save djmeph/7ebae543af52a87191e6b7fe03c8d16b to your computer and use it in GitHub Desktop.
Kubernetes and Rancher on a Raspberry Pi 5 Cluster

Deploy Kubernetes and Rancher on a Rasperry Pi 5 Cluster

I purchased three 8GB Raspberry Pi 5s with 128GB 96mb/s microSD cards to build my first Kubernetes cluster, and as expected, I ran into a lot of problems with the newness of the Pi 5. I have been working with Kubernentes professionally for exactly one year now, so for me this was about learning how to create my own cluster from the ground up to fill in the gaps of knowledge of the infrastructure that has been provided to me.

I have since upgraded this cluster to 7 nodes with 512GB storage each.

Many tutorials and examples exist on building a Raspberry Pi K8S cluster. The problems I ran into were mostly in two categories:

  1. Lack of support for Raspberry Pi 5
  2. Examples with deprecated features

A lot of what I did here was taking from many different tutorials and guides and then using the documentation for each component to figure out how the deprecated changes could work in the same or similar way. Keep in mind that most of these steps should still work for any ARM64 Raspberry Pis, and there's no reason why you can't mix and match. However, this guide is designed to leverage the current support for Raspberry Pi 5.

This tutorial uses the following components:

  1. Ubuntu Server 24.04.2 LTS for the OS (64-bit required)
  2. K3s for Kubernetes (1.31) distribution
  3. MetalLB for load balancer
  4. nginx for ingress
  5. cert-manager for certificate issuer
  6. Rancher for management

Prepare the Rasbperry Pis

K3s only requires one instance at minimum, however I recommend at least three Raspberry Pis to provide a base level of resiliency. The primary Pi will be the control plane running K3s server, and the secondary Pis will be the worker nodes running the K3s agent. I highly recommend setting up each Pi with an SSH connection that you can authenticate with through your local network. However, that will not be covered in this guide, and using a monitor and keyboard is still valid.

  1. Flash the microSD card for each Raspberry Pi with Ubuntu 24.04.2 Server 64-bit
  2. Assign a static or reserved IP address for the control plane
  3. Use Snap to install helm on the control plane
  4. Give each node a unique hostname. For this example I labeled them as ubuntu-k8s-01, ubuntu-k8s-02, ubuntu-k8s-03
  5. Install the prerequisite helm charts on the control plane:
helm repo add metallb https://metallb.github.io/metallb
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm repo add rancher-latest https://releases.rancher.com/server-charts/latest
helm repo update

Once these steps are complete, ensure that you are able to connect to the control plane from the worker nodes by pinging it.

Deploy K3s

On the control plane run the following commands:

export K3S_KUBECONFIG_MODE="644"
export INSTALL_K3S_EXEC="--disable traefik --disable servicelb"
export INSTALL_K3S_VERSION="v1.31.2+k3s1"
export K3S_NODE_NAME="<HOSTNAME>" # ie. ubuntu-k8s-01
curl -sfL https://get.k3s.io | sh -s -

# Get the token from the control node and save this for the next steps
sudo cat /var/lib/rancher/k3s/server/node-token

On the worker nodes run the following commands:

export K3S_TOKEN="<TOKEN>"
export K3S_URL="https://<CONTROL_NODE_IP_ADDRESS>:6443"
export K3S_NODE_NAME="<HOSTNAME>" # ie. ubuntu-k8s-02, ubuntu-k8s-03 ...
export INSTALL_K3S_VERSION="v1.31.2+k3s1"
curl -sfL https://get.k3s.io | sh -s -

Go back to the control plane and check on the progress. It may take a minute or two for the worker nodes to propagate. Kubectl will be automatically installed by the k3s script.

kubectl get nodes

NAME            STATUS   ROLES                  AGE     VERSION
ubuntu-k8s-01   Ready    control-plane,master   2m53s   v1.31.2+k3s1
ubuntu-k8s-02   Ready    <none>                 26s     v1.31.2+k3s1
ubuntu-k8s-03   Ready    <none>                 11s     v1.31.2+k3s1

Once all of your nodes have the ready status, use the following command for each worker node to label them:

kubectl label nodes <HOSTNAME> kubernetes.io/role=worker

Run kubectl get nodes again to verify the roles:

NAME            STATUS   ROLES                  AGE     VERSION
ubuntu-k8s-01   Ready    control-plane,master   5m31s   v1.31.2+k3s1
ubuntu-k8s-02   Ready    worker                 3m4s    v1.31.2+k3s1
ubuntu-k8s-03   Ready    worker                 2m49s   v1.31.2+k3s1

At this point you can log out of the worker nodes. Everything from here will be done on the control plane.

Deploy Load Balancer

You will need to reserve a range of IPs that are outside of the DHCP pool for this step. For my simple home router setup I chose the range 192.168.0.200-192.168.0.250 which gives me a Load Balancer pool of 50 IP addresses.

Create a file named metallb-ip-range.yaml and populate it with the following YAML:

# metallb-ip-range.yaml
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
  name: default-pool
  namespace: metallb-system
spec:
  addresses:
  - <IP_RANGE_LOWER>-<IP_RANGE_UPPER> # ie. 192.168.0.200-192.168.0.250
---
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
  name: default
  namespace: metallb-system
spec:
  ipAddressPools:
  - default-pool

Install MetalLB with the following commands:

helm upgrade metallb metallb/metallb --install --create-namespace --namespace metallb-system --wait

Deploy the IP Address Pool:

kubectl apply -f metallb-ip-range.yaml 

Deploy Ingress

Run the following command:

helm upgrade ingress-nginx ingress-nginx/ingress-nginx --install --create-namespace --namespace ingress-nginx --wait

Once this is finished, your LoadBalancer resource should be populated with an external IP from the IP range in the last section:

Run kubectl get svc -n ingress-nginx

NAME                                 TYPE           CLUSTER-IP      EXTERNAL-IP     PORT(S)                      AGE
ingress-nginx-controller-admission   ClusterIP      10.43.211.147   <none>          443/TCP                      28s
ingress-nginx-controller             LoadBalancer   10.43.50.249    192.168.0.200   80:31799/TCP,443:32238/TCP   28s

Setup Port Forwarding

Now you will want to setup port forwarding on your router to expose your WAN IP to the Load Balancer EXTERNAL-IP from the output of the last step. In this case, my LoadBalancer IP is 192.168.0.200.

  • Port 80 -> 192.168.0.200:80
  • Port 443 -> 192.168.0.200:443

Setup your DNS record for Rancher

Create a DNS record that points to your public WAN IP address. This is usually an A Record (IP address) or a CNAME record (pointing to the A Record of the public IP address)

  • A rancher.example.com <PUBLIC_IP_ADDRESS>
  • CNAME rancher.example.com wan.example.com

Deploy cert-manager

Install cert-manager CRDs and controller in one step

kubectl apply -f https://github.com/jetstack/cert-manager/releases/download/v1.17.0/cert-manager.yaml

Deploy the production and staging ClusterIssuers. Create two yaml files certmanager-clusterissuer-staging.yaml and certmanager-clusterissuer-production.yaml

# certmanager-clusterissuer-staging.yaml
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-staging
  namespace: cert-manager
spec:
  acme:
    server: https://acme-staging-v02.api.letsencrypt.org/directory
    email: [email protected]
    privateKeySecretRef:
      name: letsencrypt-staging
    solvers:
      - http01:
          ingress:
            ingressClassName: nginx
# certmanager-clusterissuer-production.yaml
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-production
  namespace: cert-manager
spec:
  acme:
    server: https://acme-v02.api.letsencrypt.org/directory
    email: [email protected]
    privateKeySecretRef:
      name: letsencrypt-production
    solvers:
      - http01:
          ingress:
            ingressClassName: nginx

Deploy the ClusterIssuers:

kubectl apply -f certmanager-clusterissuer-staging.yaml
kubectl apply -f certmanager-clusterissuer-production.yaml

Deploy Rancher

Create a file called rancher-values.yaml and populate with the following code:

# rancher-values.yaml
hostname: rancher.example.com # From the DNS record created in previous steps
ingress:
  tls:
    source: secret
  extraAnnotations:
    cert-manager.io/cluster-issuer: letsencrypt-production
  ingressClassName: nginx
letsEncrypt:
  email: [email protected] # Use the email address you wish to receive certificate expiration alerts
  ingress:
    class: nginx

Run the following command:

helm upgrade rancher rancher-latest/rancher --install --create-namespace --namespace cattle-system --values rancher-values.yaml

Here are some commands to use to check on the status of your certificate request:

kubectl -n cattle-system get certificate
kubectl -n cattle-system get issuer
kubectl -n cattle-system get certificaterequest
kubectl -n cattle-system describe certificaterequest tls-rancher-ingress-1 # from the output of the last command

This may take a while. Eventually the certificate will be in the ready state:

kubectl -n cattle-system get certificate

NAME                READY   SECRET              AGE
ingress-nginx-tls   True    ingress-nginx-tls   7m23s

Now you can generate a URL to authenticate with the Rancher Dashboard:

echo https://rancher.example.com/dashboard/?setup=$(kubectl get secret --namespace cattle-system bootstrap-secret -o go-template='{{.data.bootstrapPassword|base64decode}}')

This will output the full URL. Paste it in your browser and configure your Rancher inital setup.

Other considerations.

Once you are setup with Rancher, go to Cluster -> Tools and install some helpful utilities.

  • Monitoring - Get valuable insights on resource usage and allocation, network activity, and more with Prometheus and Grafana.
  • Longhorn - Creates a storage class for managed persistent volumes. Out of the box, this will create three replicas for every persistent volume claim, so if you lose a worker node, Longhorn will automatically restore the persistent volumes from a replica.
  • Rancher Backups - Save backups of your Rancher that can be restored in a catostrophic failure of the control plane.
@ZaBovv
Copy link

ZaBovv commented Oct 24, 2024

hi, i tried you'r guide but i'm stuck with the install of metallb

when i use:
helm upgrade --install metallb metallb/metallb --create-namespace --namespace metallb-system --wait
i get:

Error: Kubernetes cluster unreachable: Get "http://localhost:8080/version": dial tcp [::1]:8080: connect: connection refused

can you help me solve this problem?

@jhenry76
Copy link

hi, i tried you'r guide but i'm stuck with the install of metallb

when i use: helm upgrade --install metallb metallb/metallb --create-namespace --namespace metallb-system --wait i get:

Error: Kubernetes cluster unreachable: Get "http://localhost:8080/version": dial tcp [::1]:8080: connect: connection refused

can you help me solve this problem?

try export KUBECONFIG=/etc/rancher/k3s/k3s.yaml and run again

@ZaBovv
Copy link

ZaBovv commented Dec 3, 2024

yes that solved my problem, now i have another problem..
if i have a domain like name.ex my yaml where you set rancher.example.com should be rancher.name.ex right?

i set a CNAME like rancher.name.ex -> name.ex and a A like name.ex -> PUBLIC IP

when i run kubectl -n cattle-system describe certificaterequest ingress-nginx-tls-1 i get

Normal  WaitingForApproval  15m   cert-manager-certificaterequests-issuer-acme        Not signing CertificateRequest until it is Approved
  Normal  WaitingForApproval  15m   cert-manager-certificaterequests-issuer-selfsigned  Not signing CertificateRequest until it is Approved
  Normal  WaitingForApproval  15m   cert-manager-certificaterequests-issuer-vault       Not signing CertificateRequest until it is Approved
  Normal  WaitingForApproval  15m   cert-manager-certificaterequests-issuer-ca          Not signing CertificateRequest until it is Approved
  Normal  WaitingForApproval  15m   cert-manager-certificaterequests-issuer-venafi      Not signing CertificateRequest until it is Approved
  Normal  cert-manager.io     15m   cert-manager-certificaterequests-approver           Certificate request has been approved by cert-manager.io
  Normal  IssuerNotReady      15m   cert-manager-certificaterequests-issuer-ca          Referenced issuer does not have a Ready status condition
  Normal  CertificateIssued   10m   cert-manager-certificaterequests-issuer-ca          Certificate fetched from issuer successfully

for the other commands

kubectl -n cattle-system get certificate
kubectl -n cattle-system get issuer
kubectl -n cattle-system get certificaterequest

i get APPROVED True

and when i run
kubectl get all --all-namespaces

NAMESPACE                         NAME                                           READY   STATUS      RESTARTS      AGE
kube-system                       pod/local-path-provisioner-6c86858495-kdxp7    1/1     Running     0             74m
kube-system                       pod/coredns-6799fbcd5-v4f7m                    1/1     Running     0             74m
kube-system                       pod/metrics-server-67c658944b-m7tfp            1/1     Running     0             74m
metallb-system                    pod/metallb-controller-76bf5df6db-gvpkw        1/1     Running     1 (72m ago)   72m
metallb-system                    pod/metallb-speaker-frq7t                      4/4     Running     0             72m
default                           pod/ingress-nginx-controller-ddf5756b8-hz9bh   1/1     Running     0             68m
cert-manager                      pod/cert-manager-cainjector-9b74bc658-shfmb    1/1     Running     0             66m
cert-manager                      pod/cert-manager-796cbd6574-pxwk9              1/1     Running     0             66m
cert-manager                      pod/cert-manager-webhook-7ddfd7c4bd-sbc5j      1/1     Running     0             66m
cattle-system                     pod/rancher-75c5b84ff9-lnghh                   1/1     Running     0             66m
cattle-system                     pod/rancher-75c5b84ff9-jht28                   1/1     Running     0             66m
cattle-system                     pod/rancher-75c5b84ff9-5c99q                   1/1     Running     1 (64m ago)   66m
cattle-fleet-system               pod/gitjob-656cbd6c99-45bt8                    1/1     Running     0             62m
cattle-fleet-system               pod/fleet-controller-79d5d5df7b-xxd4g          3/3     Running     0             62m
cattle-fleet-local-system         pod/fleet-agent-0                              2/2     Running     0             61m
cattle-system                     pod/rancher-webhook-5bdd99bdd7-c5n5w           1/1     Running     0             61m
cattle-provisioning-capi-system   pod/capi-controller-manager-5967c7487f-srb76   1/1     Running     0             60m
cattle-system                     pod/helm-operation-qk4lp                       0/2     Completed   0             2m44s

NAMESPACE                         NAME                                         TYPE           CLUSTER-IP      EXTERNAL-IP     PORT(S)                      AGE
default                           service/kubernetes                           ClusterIP      10.43.0.1       <none>          443/TCP                      75m
kube-system                       service/kube-dns                             ClusterIP      10.43.0.10      <none>          53/UDP,53/TCP,9153/TCP       74m
kube-system                       service/metrics-server                       ClusterIP      10.43.124.239   <none>          443/TCP                      74m
metallb-system                    service/metallb-webhook-service              ClusterIP      10.43.165.139   <none>          443/TCP                      72m
default                           service/ingress-nginx-controller-admission   ClusterIP      10.43.30.163    <none>          443/TCP                      68m
cert-manager                      service/cert-manager-webhook                 ClusterIP      10.43.63.190    <none>          443/TCP                      66m
cert-manager                      service/cert-manager                         ClusterIP      10.43.19.13     <none>          9402/TCP                     66m
cattle-system                     service/rancher                              ClusterIP      10.43.197.144   <none>          80/TCP,443/TCP               66m
default                           service/ingress-nginx-controller             LoadBalancer   10.43.133.143   192.168.1.215   80:32343/TCP,443:31600/TCP   68m
cattle-fleet-system               service/gitjob                               ClusterIP      10.43.71.136    <none>          80/TCP                       62m
cattle-fleet-system               service/monitoring-fleet-controller          ClusterIP      10.43.222.37    <none>          8080/TCP                     62m
cattle-fleet-system               service/monitoring-gitjob                    ClusterIP      10.43.192.54    <none>          8081/TCP                     62m
cattle-system                     service/rancher-webhook                      ClusterIP      10.43.97.70     <none>          443/TCP                      61m
cattle-fleet-local-system         service/fleet-agent                          ClusterIP      None            <none>          <none>                       61m
cattle-provisioning-capi-system   service/capi-webhook-service                 ClusterIP      10.43.42.148    <none>          443/TCP                      60m

NAMESPACE        NAME                             DESIRED   CURRENT   READY   UP-TO-DATE   AVAILABLE   NODE SELECTOR            AGE
metallb-system   daemonset.apps/metallb-speaker   1         1         1       1            1           kubernetes.io/os=linux   72m

NAMESPACE                         NAME                                       READY   UP-TO-DATE   AVAILABLE   AGE
kube-system                       deployment.apps/local-path-provisioner     1/1     1            1           74m
kube-system                       deployment.apps/coredns                    1/1     1            1           74m
kube-system                       deployment.apps/metrics-server             1/1     1            1           74m
metallb-system                    deployment.apps/metallb-controller         1/1     1            1           72m
cert-manager                      deployment.apps/cert-manager-cainjector    1/1     1            1           66m
cert-manager                      deployment.apps/cert-manager               1/1     1            1           66m
cert-manager                      deployment.apps/cert-manager-webhook       1/1     1            1           66m
default                           deployment.apps/ingress-nginx-controller   1/1     1            1           68m
cattle-system                     deployment.apps/rancher                    3/3     3            3           66m
cattle-fleet-system               deployment.apps/gitjob                     1/1     1            1           62m
cattle-fleet-system               deployment.apps/fleet-controller           1/1     1            1           62m
cattle-system                     deployment.apps/rancher-webhook            1/1     1            1           61m
cattle-provisioning-capi-system   deployment.apps/capi-controller-manager    1/1     1            1           60m

NAMESPACE                         NAME                                                 DESIRED   CURRENT   READY   AGE
kube-system                       replicaset.apps/local-path-provisioner-6c86858495    1         1         1       74m
kube-system                       replicaset.apps/coredns-6799fbcd5                    1         1         1       74m
kube-system                       replicaset.apps/metrics-server-67c658944b            1         1         1       74m
metallb-system                    replicaset.apps/metallb-controller-76bf5df6db        1         1         1       72m
cert-manager                      replicaset.apps/cert-manager-cainjector-9b74bc658    1         1         1       66m
cert-manager                      replicaset.apps/cert-manager-796cbd6574              1         1         1       66m
cert-manager                      replicaset.apps/cert-manager-webhook-7ddfd7c4bd      1         1         1       66m
default                           replicaset.apps/ingress-nginx-controller-ddf5756b8   1         1         1       68m
cattle-system                     replicaset.apps/rancher-75c5b84ff9                   3         3         3       66m
cattle-fleet-system               replicaset.apps/gitjob-656cbd6c99                    1         1         1       62m
cattle-fleet-system               replicaset.apps/fleet-controller-79d5d5df7b          1         1         1       62m
cattle-system                     replicaset.apps/rancher-webhook-5bdd99bdd7           1         1         1       61m
cattle-provisioning-capi-system   replicaset.apps/capi-controller-manager-5967c7487f   1         1         1       60m

NAMESPACE                   NAME                           READY   AGE
cattle-fleet-local-system   statefulset.apps/fleet-agent   1/1     61m

if all of this is correct maybe i have a problem with my modem and port forwarding.

i just want to know if what i have done is correct for the domain.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment