This post documents the end-to-end process for setting up a k3s Kubernetes cluster on a Raspberry Pi, managing it remotely from a Mac, and deploying applications securely using GitOps with FluxCD and SOPS encryption. We'll cover everything from OS install to deploying encrypted secrets and tackling common troubleshooting hurdles.
1. Initial Pi Setup & OS Installation
This phase covers preparing the Raspberry Pi hardware and operating system.
- Install OS: Use the Raspberry Pi Imager to write Raspberry Pi OS (64-BIT) to an SD card.
- OS Configuration: In the Imager's advanced settings, pre-configure:
-
Hostname:
k3s-node(or your preferred name) -
Username and Password: e.g.,
pi-admin - Wireless LAN: Your Wi-Fi SSID and password.
-
Hostname:
- Set a Static IP: To ensure a stable connection, set a DHCP Reservation for the Pi in your home router's settings, linking the Pi's MAC address to a specific IP (e.g.,
192.168.1.100).
2. Kubernetes (k3s) Installation
We installed k3s, a lightweight Kubernetes distribution, directly onto the Pi's operating system.
- Enable Cgroups (Critical Fix): The
k3sservice will crash on startup without this Linux kernel feature.- SSH into the Pi:
ssh pi-admin@k3s-node.local - Edit the boot config file:
sudo nano /boot/firmware/cmdline.txt - Add
cgroup_memory=1 cgroup_enable=memoryto the end of the single line in the file. - Save (
Ctrl+X,Y,Enter) and reboot:sudo reboot.
- SSH into the Pi:
-
Install k3s:
# On the Pi curl -sfL [https://get.k3s.io](https://get.k3s.io) | sh - -
Verify Service: Ensure the
k3sservice is stable and running.
# On the Pi sudo systemctl status k3s.serviceThe output must show
Active: active (running).
3. Remote Management from macOS
To manage the Pi's cluster from your Mac, you need to copy its configuration.
- Update Kubeconfig File:
- On the Pi, copy the config:
sudo cat /etc/rancher/k3s/k3s.yaml > k3s_config.yaml - Edit the file (
nano k3s_config.yaml) and change theserveraddress fromhttps://127.0.0.1:6443to the Pi's static IP (e.g.,https://192.168.1.100:6443).
- On the Pi, copy the config:
-
Copy to Mac: From your Mac's terminal, copy the file to your local kubeconfig location. Warning: This overwrites your default config. If you manage multiple clusters, merge this file's contents manually.
scp pi-admin@k3s-node.local:~/k3s_config.yaml ~/.kube/config -
Test Connection:
kubectl get nodesYou should see your Pi node (
k3s-node) listed.
4. GitOps Setup with Flux & SOPS
This phase automates deployments and configures secret encryption.
-
Bootstrap Flux: Install Flux on the cluster and configure it to watch your Git repository.
# On your Mac # (Ensure GITHUB_USER is set in your env) flux bootstrap github \ --owner=$GITHUB_USER \ --repository=pi-cluster \ --branch=main \ --path=./clusters/staging \ --personal -
Generate
ageKeypair: Create a new keypair for encryption.
# On your Mac age-keygen -o age.agekeyThis creates
age.agekey(your private key) and shows your public key (startsage1...). Keep the private key safe! -
Add Private Key to Cluster: Create a Kubernetes secret in the
flux-systemnamespace containing your private key. This allows Flux's controllers to decrypt files.
# On your Mac cat age.agekey | kubectl create secret generic sops-age \ --namespace=flux-system \ --from-file=age.agekey=/dev/stdin -
Configure SOPS Rules: Create a
.sops.yamlfile inclusters/staging/to tell SOPS which public key to use for encrypting files.
# In clusters/staging/.sops.yaml creation_rules: - path_regex: .*.yaml encrypted_regex: ^(data|stringData)$ age: <PASTE_YOUR_PUBLIC_AGE_KEY_HERE> -
Configure Flux for Decryption: Edit
clusters/staging/flux-system/kustomization.yamlto tell Flux to use thesops-agesecret.
# In clusters/staging/flux-system/kustomization.yaml spec: # ... decryption: provider: sops secretRef: name: sops-age Commit & Push your new
.sops.yamland modifiedkustomization.yamlto Git.
5. Deploying Encrypted Secrets via GitOps
This is the process of creating encrypted secret files and adding them to your Git repository for Flux to deploy.
1. : Deploy the Cloudflare Secret
-
Generate Secret YAML: Create a YAML manifest from your Cloudflare credential file (
<tunnel_id>.json).
kubectl create secret generic tunnel-credentials \ --from-file=credentials.json=./<tunnel_id>.json \ --namespace <your-app-namespace> \ --dry-run=client -o yaml > cloudflare-secret.yaml -
Encrypt the File:
sops --config clusters/staging/.sops.yaml --encrypt --in-place cloudflare-secret.yaml Move and Rename: Move the encrypted secret into your application's directory (e.g.,
apps/base/linkding/secret-cloudflare.sops.yaml).
2: Deploy the Linkding Superuser Secret
-
Generate Secret YAML: Create a secret with the environment variables
linkdingexpects.
kubectl create secret generic linkding-superuser \ --from-literal=LD_SUPERUSER_NAME=your-user \ --from-literal=LD_SUPERUSER_PASSWORD=YourSecurePassword \ --namespace <your-app-namespace> \ --dry-run=client -o yaml > secret-superuser.yaml -
Encrypt and Move:
sops --config clusters/staging/.sops.yaml --encrypt --in-place secret-superuser.yaml mv secret-superuser.yaml apps/base/linkding/secret-superuser.sops.yaml -
Update
kustomization.yaml: Editapps/base/linkding/kustomization.yamlto tell Flux to deploy these new secret files.
resources: - namespace.yaml - deployment.yaml # ... other resources - secret-cloudflare.sops.yaml - secret-superuser.sops.yaml -
Update
deployment.yaml: Modifyapps/base/linkding/deployment.yamlto use the superuser secret.
# In deployment.yaml, inside the container spec: envFrom: - secretRef: name: linkding-superuser
Final Step: Commit and Reconcile
After adding the files and updating the Kustomizations, commit everything to Git. Flux will automatically sync the changes, decrypt the secrets, and deploy them to your cluster.
git add .
git commit -m "Feat: Add encrypted secrets for Cloudflare and Linkding"
git push origin main
6. Troubleshooting Common Issues
ImagePullBackOff
Symptom: Kubernetes can't download the container image. You see this status when you run kubectl get pods.
-
Cause 1: Wrong Architecture. You're trying to run an
amd64(standard PC/server) image on yourarm64Raspberry Pi. Solution 1: Find a multi-arch image or an
arm64/aarch64specific version. Look for tags like-arm64,-aarch64, or check image descriptions on Docker Hub/GHCR.lscr.io(LinuxServer.io) often provides good multi-arch images. Update theimage:tag in yourdeployment.yamlandgit push.Cause 2: Pi Network/DNS Issues. The Pi itself can't reach the container registry.
-
Solution 2: SSH into the Pi.
- Test basic connectivity:
ping google.com - Test DNS:
nslookup ghcr.io - If DNS fails, try setting static DNS servers: edit
/etc/dhcpcd.conf(sudo nano /etc/dhcpcd.conf). - Add a line:
static domain_name_servers=8.8.8.8 1.1.1.1(Google/Cloudflare DNS). - Save and reboot (
sudo reboot).
- Test basic connectivity:
Pod Stuck in Pending
Symptom: The pod stays in the Pending state and never starts. Running kubectl describe pod <pod-name> shows an event like failed to bind volume.
-
Cause: The pod is waiting for a
PersistentVolumeClaim(PVC), but no suitablePersistentVolume(PV) is available to fulfill it (e.g., wrong size, access mode, storage class, or no PVs exist). -
Solution (Simple
hostPathPV for Homelab): Define aPersistentVolumein your GitOps repo that uses a directory on the Pi's filesystem. Warning:hostPathties the data to that specific Pi node.
-
Create a directory on the Pi:
sudo mkdir -p /mnt/data/my-app-data && sudo chown nobody:nogroup /mnt/data/my-app-data(Adjust owner if your app runs as a different user).
-
Create a
pv.yamlmanifest in your GitOps repo (e.g., alongside the PVC):
apiVersion: v1 kind: PersistentVolume metadata: name: my-app-data-pv # Unique name spec: capacity: storage: 5Gi # Must be >= PVC request volumeMode: Filesystem accessModes: - ReadWriteOnce # Must match PVC persistentVolumeReclaimPolicy: Retain # Keep data if PV is deleted storageClassName: manual # Give it a name hostPath: path: "/mnt/data/my-app-data" # Path on the Pi node Add
pv.yamlto your Kustomization.Update your application's PVC
spec.storageClassNametomanual(or whatever name you chose) so it binds to this PV.
Connection Refused / ServiceUnavailable (from remote kubectl)
Symptom: Running kubectl get nodes from your Mac or remote machine fails with ServiceUnavailable or connection refused.
-
Cause: The
k3sservice on the Pi is down, restarting, or unstable. This is almost always caused by:- Forgetting the
cgroupsfix (critical forkson Raspberry Pi OS). - The Pi is out of resources (memory/CPU).
- Forgetting the
-
Solution:
- SSH into the Pi.
- Check the
k3sservice status:sudo systemctl status k3s.service. - If it's not
active (running), check the logs for crash reasons:sudo journalctl -u k3s.service -f. - Confirm
/boot/firmware/cmdline.txtincludes thecgroup_memory=1 cgroup_enable=memoryflags (and reboot if you had to add them). - Check resource usage with
htop.
Top comments (0)