https://github.com/baiganio/k3s-pi5
Quick instructions on how to setup k3s on Raspberry Pi 5 and tunnel domain name with Cloudflare
https://github.com/baiganio/k3s-pi5
instructions k3s raspberry-pi
Last synced: 2 months ago
JSON representation
Quick instructions on how to setup k3s on Raspberry Pi 5 and tunnel domain name with Cloudflare
- Host: GitHub
- URL: https://github.com/baiganio/k3s-pi5
- Owner: BaiGanio
- License: apache-2.0
- Created: 2026-04-10T09:02:51.000Z (3 months ago)
- Default Branch: master
- Last Pushed: 2026-04-10T13:30:57.000Z (3 months ago)
- Last Synced: 2026-04-10T15:24:20.598Z (3 months ago)
- Topics: instructions, k3s, raspberry-pi
- Language: JavaScript
- Homepage: https://baiganio.github.io/k3s-pi5/
- Size: 68.4 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# 👉 [https://baiganio.github.io/k3s-pi5](https://baiganio.github.io/k3s-pi5/)
### Step 3: Grant Non-Root Access to kubectl (Optional)
To avoid typing `sudo` every time:
```bash
# Create kubeconfig at user level
mkdir -p ~/.kube
sudo k3s kubectl config view --raw | sudo tee ~/.kube/config > /dev/null
sudo chown $(id -u):$(id -g) ~/.kube/config
sudo chmod 600 ~/.kube/config
# Now you can use kubectl without sudo
kubectl get nodes
```
### Step 4: Verify Docker/containerd
K3s uses containerd (not Docker) by default. Check it's working:
```bash
# List running containers
sudo crictl ps
# This shows all containers managed by k3s itself
# (e.g., coredns, metrics-server, local-path-provisioner)
```
**Why containerd instead of Docker:** Containerd is lighter and more suitable for resource-constrained environments.
---
## Cloudflare Tunnel Configuration
### What's Happening Here
You'll configure Cloudflare Tunnel (formerly Argo Tunnel) to:
1. Route external traffic from your Cloudflare domain → your Pi's k3s Nginx Ingress
2. Create a secure tunnel without exposing your home IP
3. Let you access apps like `app.yourdomain.com`
### Step 1: Install Cloudflare Tunnel Agent
```bash
# Download the latest cloudflared binary for ARM64
wget -q https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-arm64
# Make it executable
chmod +x cloudflared-linux-arm64
# Move to system path
sudo mv cloudflared-linux-arm64 /usr/local/bin/cloudflared
# Verify installation
cloudflared --version
```
### Step 2: Authenticate with Cloudflare
```bash
# This opens your browser to Cloudflare login
cloudflared tunnel login
# You'll:
# 1. Log in to your Cloudflare account
# 2. Select your domain
# 3. A cert file is saved locally
# Check it was saved
cat ~/.cloudflared/cert.pem
# Should show: -----BEGIN CERTIFICATE-----
```
### Step 3: Create a Tunnel
```bash
# Create a new tunnel (replace "my-pi" with a meaningful name)
cloudflared tunnel create my-pi
# Output:
# Tunnel credentials written to ~/.cloudflared/[tunnel-id].json
# Tunnel my-pi created with ID [tunnel-id]
# Save the tunnel ID for next steps
TUNNEL_ID=$(cloudflared tunnel list | grep my-pi | awk '{print $1}')
echo $TUNNEL_ID
```
### Step 4: Configure Tunnel Routing
Create a config file at `~/.cloudflared/config.yml`:
```bash
nano ~/.cloudflared/config.yml
```
Add this content (adjust domains as needed):
```yaml
tunnel: my-pi # Match the tunnel name from Step 3
credentials-file: /home/pi/.cloudflared/[TUNNEL_ID].json
# Replace [TUNNEL_ID] with actual tunnel ID from previous step
ingress:
# Route your domain to k3s Nginx Ingress (running on port 80)
- hostname: yourdomain.com
service: http://localhost/
- hostname: "*.yourdomain.com"
service: http://localhost/
# Catch-all for any other traffic
- service: http_status:404
```
**What this does:**
- `hostname: yourdomain.com` → Routes root domain to your k3s Ingress
- `"*.yourdomain.com"` → Routes all subdomains (app.yourdomain.com, api.yourdomain.com, etc.)
- `service: http_status:404` → Returns 404 for unmapped hostnames
### Step 5: Test Tunnel Connection
```bash
# Start tunnel manually to test
cloudflared tunnel run my-pi
# You should see:
# ... tunnel registered with Cloudflare
# ... routing rules active
# ... connected to edge
# Press Ctrl+C to stop (you'll create a systemd service next)
```
### Step 6: Create Systemd Service for Tunnel
```bash
# Create systemd unit file
sudo nano /etc/systemd/system/cloudflared.service
```
Add:
```ini
[Unit]
Description=Cloudflare Tunnel
After=network.target
Wants=network-online.target
[Service]
Type=simple
User=pi
WorkingDirectory=/home/pi/.cloudflared
ExecStart=/usr/local/bin/cloudflared tunnel run my-pi
Restart=on-failure
RestartSec=5s
[Install]
WantedBy=multi-user.target
```
```bash
# Enable and start the service
sudo systemctl daemon-reload
sudo systemctl enable cloudflared
sudo systemctl start cloudflared
# Check status
sudo systemctl status cloudflared
# View logs
sudo systemctl journalctl -u cloudflared -f
```
---
## Persistent Storage Setup
### What's Happening
For databases (PostgreSQL, MySQL) to survive pod restarts, we need persistent storage. K3s provides a built-in storage provisioner, but we'll also set up proper volumes.
### Step 1: Check Default Storage Class
K3s ships with a "local-path-provisioner" storage class:
```bash
# List storage classes
kubectl get storageclass
# Expected output:
# NAME PROVISIONER RECLAIM POLICY
# local-path (default) rancher.io/local-path Delete
```
This stores data at `/var/lib/rancher/k3s/storage/` on the Pi.
### Step 2: Create a Data Directory for Databases
For better organization, create a dedicated directory:
```bash
# Create directory for persistent data
sudo mkdir -p /mnt/k3s-data
sudo mkdir -p /mnt/k3s-data/databases
sudo mkdir -p /mnt/k3s-data/applications
# Set permissions (allow k3s user to access)
sudo chown -R 1000:1000 /mnt/k3s-data
sudo chmod -R 755 /mnt/k3s-data
```
**Why:**
- Separates Kubernetes data from OS system files
- Easier to back up or expand storage later
- If needed, you can mount an external USB drive here
### Step 3: Create Persistent Volume (Manual Example)
For testing, we'll create a PersistentVolume manually:
```bash
# Create a manifest for persistent storage
nano pv-example.yaml
```
Add:
```yaml
apiVersion: v1
kind: PersistentVolume
metadata:
name: database-pv
spec:
capacity:
storage: 5Gi
accessModes:
- ReadWriteOnce
storageClassName: local-path
hostPath:
path: "/mnt/k3s-data/databases"
type: Directory
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: database-pvc
namespace: default
spec:
accessModes:
- ReadWriteOnce
storageClassName: local-path
resources:
requests:
storage: 5Gi
```
Apply it:
```bash
kubectl apply -f pv-example.yaml
# Verify
kubectl get pv
kubectl get pvc
```
**What this does:**
- **PersistentVolume (PV):** Allocates 5GB from `/mnt/k3s-data/databases`
- **PersistentVolumeClaim (PVC):** Applications claim storage through this
---
## Nginx Ingress Controller
### What's an Ingress?
An Ingress is a Kubernetes resource that:
- Routes external traffic to internal services
- Handles hostname-based routing (example.com → Service A, api.example.com → Service B)
- Provides load balancing and SSL termination
### Step 1: Check If Traefik is Already Installed
K3s comes with Traefik (an ingress controller) by default:
```bash
# Check Traefik pods
kubectl get pods -n kube-system | grep traefik
# Expected: traefik pod should be running
```
Traefik is simpler than Nginx for k3s. We'll use it, but if you prefer Nginx, see the Alternative section below.
### Step 2: Verify Traefik Service
```bash
# Check Traefik service
kubectl get svc -n kube-system traefik
# Expected output shows:
# TYPE: LoadBalancer
# EXTERNAL-IP: 192.168.x.x (your Pi's IP)
# PORT(S): 80:xxxxx/TCP, 443:xxxxx/TCP
```
This means Traefik is already listening on ports 80 and 443.
### Step 3: Test with a Simple Ingress
Create a test ingress:
```bash
nano test-ingress.yaml
```
Add:
```yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: test-ingress
annotations:
# Traefik annotation (different from standard Nginx)
traefik.ingress.kubernetes.io/router.entrypoints: web
spec:
rules:
- host: test.yourdomain.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: nginx-test
port:
number: 80
```
```bash
kubectl apply -f test-ingress.yaml
```
---
## K3s Dashboard
### What's the Dashboard?
A web UI to visualize:
- Pods, Services, Deployments
- Resource usage (CPU, memory)
- Logs from containers
- Deploy new applications
### Step 1: Install K3s Dashboard
```bash
# K3s doesn't include dashboard by default, install it:
kubectl apply -f https://raw.githubusercontent.com/kubernetes/dashboard/v2.7.0/aio/deploy/recommended.yaml
# Wait for dashboard pod to start (takes ~30 seconds)
kubectl wait --for=condition=ready pod \
-l k8s-app=kubernetes-dashboard \
-n kubernetes-dashboard \
--timeout=300s
```
### Step 2: Create Admin User and Get Access Token
```bash
# Create admin user
kubectl apply -f - < nginx.yourdomain.com 192.168.x.x
# default dotnet-hello dotnet.yourdomain.com 192.168.x.x
```
### Step 3: Test DNS Resolution
```bash
# From your Pi, test if Cloudflare tunnel is routing correctly
curl -H "Host: nginx.yourdomain.com" http://localhost/
# Expected: HTML content from Nginx welcome page (or your dashboard)
```
### Step 4: Monitor Cloudflare Tunnel
```bash
# Check tunnel status
cloudflared tunnel list
# View recent connections
cloudflared tunnel info my-pi
```
### Step 5: Monitor Resource Usage
```bash
# Check node resource usage
kubectl top nodes
# Check pod resource usage
kubectl top pods -A
# Example output:
# NAME CPU(cores) MEMORY(Mi)
# nginx-welcome-... 5m 32Mi
# dotnet-hello-... 10m 64Mi
# postgres-... 20m 128Mi
```
This shows how much CPU and memory each pod is using.
### Step 6: View Pod Logs
```bash
# View logs from specific pod
kubectl logs -l app=nginx
# Follow logs in real-time
kubectl logs -l app=nginx -f
# View logs from .NET app
kubectl logs -l app=dotnet-hello
```
---
## Troubleshooting
### Issue 1: K3s Pod Won't Start ("ImagePullBackOff")
**Symptom:** `kubectl get pods` shows pod stuck in "ImagePullBackOff"
**Cause:** Image doesn't exist for ARM64 architecture
**Solution:**
```bash
# Check what architecture the image supports
docker inspect image-name | grep -i "Architecture"
# Use only ARM64 compatible images. Safe choices:
# - alpine (all versions)
# - debian (all versions)
# - ubuntu:22.04
# - nginx:latest
# - postgres:15-alpine
# Do NOT use:
# - Images tagged "amd64" (Intel only)
# - Images tagged "windows" or "nanoserver"
```
### Issue 2: Pod Running But App Not Responding
**Symptom:** Pod is "Running" but website shows 503 or timeout
**Cause:** Service networking issue or app crashed inside container
**Solution:**
```bash
# 1. Check if pod is actually running or crashed
kubectl describe pod
# 2. Check logs for errors
kubectl logs
# 3. Test connectivity inside cluster
kubectl exec -it -- /bin/sh
# Then inside container: curl localhost:80
# 4. Check service
kubectl get svc
# Verify CLUSTER-IP is assigned and not
```
### Issue 3: Persistent Storage Not Working
**Symptom:** Database loses data after pod restart
**Cause:** PVC not bound or wrong path
**Solution:**
```bash
# Check PVC status
kubectl get pvc
# Should show:
# NAME STATUS VOLUME ...
# database-pvc Bound pv-1 ...
# If status is "Pending":
kubectl describe pvc database-pvc
# Check PV exists
kubectl get pv
```
### Issue 4: Cloudflare Tunnel Not Routing Traffic
**Symptom:** Website shows "Error 522" or "Origin unreachable"
**Cause:** Tunnel not connected or config incorrect
**Solution:**
```bash
# Check tunnel is connected
sudo systemctl status cloudflared
# Check tunnel logs
sudo journalctl -u cloudflared -f
# Verify config has correct hostnames and ports
cat ~/.cloudflared/config.yml
# Test locally without tunnel:
curl -H "Host: nginx.yourdomain.com" http://localhost/
```
### Issue 5: High Memory Usage / Pod Eviction
**Symptom:** Pods getting killed or "MemoryLimitExceeded"
**Cause:** Pods using more memory than limit
**Solution:**
```bash
# Check current resource usage
kubectl top pods -A
# Increase limits in YAML:
# resources:
# limits:
# memory: "256Mi" # Increase from original
# Or reduce replicas:
kubectl scale deployment nginx-welcome --replicas=1
```
---
## Next Steps After Testing
1. **Add More Apps:** Create Deployments for your Node.js apps using similar patterns
2. **Enable HTTPS:** Add SSL certificate with cert-manager
3. **Backups:** Set up backup strategy for persistent storage
4. **Monitoring:** Install Prometheus and Grafana for metrics
5. **CI/CD:** Set up automated deployments with GitHub Actions
---
## Quick Reference Commands
```bash
# Pod management
kubectl get pods # List pods
kubectl describe pod # Detailed info
kubectl logs # View logs
kubectl exec -it -- /bin/sh # Shell access
kubectl delete pod # Remove pod
# Deployment management
kubectl get deployments # List deployments
kubectl scale deployment --replicas=3 # Scale up/down
kubectl set image deployment/ app=image:tag # Update image
# Service and Ingress
kubectl get svc # List services
kubectl get ingress # List ingress rules
# Resource monitoring
kubectl top nodes # Node CPU/memory
kubectl top pods # Pod CPU/memory
# Debugging
kubectl get events # Recent cluster events
kubectl describe node # Node details
```
---
## Important Security Notes
⚠️ **For Production:**
- Change all default passwords (postgres, dashboard token)
- Enable RBAC properly (current setup is admin-only)
- Use sealed secrets for credentials (not plain ConfigMap)
- Enable network policies to restrict traffic between pods
- Regularly update k3s: `sudo systemctl stop k3s && sudo systemctl start k3s`
- Back up your persistent data regularly
---
**Setup Complete!** Your k3s cluster on Raspberry Pi 5 is now ready for testing. Start with the Nginx deployment, verify it works through Cloudflare, then proceed to .NET and database applications.