Kubernetes has a reputation for being operationally complex, resource-hungry, and overkill for anything smaller than a mid-sized company. K3s — the lightweight Kubernetes distribution from Rancher/SUSE — exists to challenge that reputation. It runs comfortably on a Raspberry Pi 4 with 4 GB of RAM, installs in about 30 seconds, and is fully conformant Kubernetes. The same kubectl commands, the same manifests, the same Helm charts.
This guide walks through standing up a K3s cluster on Raspberry Pi hardware (or any homelab server), connecting multiple nodes, configuring ingress and TLS, adding persistent storage with Longhorn, and deploying real workloads. By the end you’ll have a cluster that can run most things you’d run in a cloud Kubernetes environment, on hardware that fits in a shoebox.
Why K3s for Homelab
Full Kubernetes (kubeadm clusters, EKS, GKE) has real overhead: etcd, kube-apiserver, kube-controller-manager, kube-scheduler, and the container runtime each need memory and CPU. On a cloud node with 16 GB of RAM that overhead is invisible. On a Raspberry Pi 4 with 4 GB, it’s a problem.
K3s addresses this by:
- Bundling everything (server, agent, container runtime) into a single ~70 MB binary
- Replacing etcd with SQLite for single-node and small clusters (etcd is still available for HA)
- Removing alpha features, cloud provider integrations, and storage plugins that homelab users don’t need
- Using containerd directly instead of Docker (Docker is no longer required)
- Running the control plane components as goroutines in a single process rather than separate services
The result: a K3s server on a Pi 4 idles at around 300–400 MB of RAM. A kubeadm single-node cluster idles at 1.5–2 GB. On a 4 GB Pi, that difference is everything.
K3s is also fully upstream-conformant — it passes the CNCF Kubernetes conformance tests. There are no proprietary extensions, no vendor lock-in, and no missing APIs. Anything that runs on upstream Kubernetes runs on K3s.
Hardware Requirements
Raspberry Pi Options
| Model |
RAM |
Recommended Use |
| Pi 4 (2 GB) |
2 GB |
Worker node only, light workloads |
| Pi 4 (4 GB) |
4 GB |
Single-node cluster or worker |
| Pi 4 (8 GB) |
8 GB |
Server node or worker, comfortable headroom |
| Pi 5 (4 GB) |
4 GB |
Excellent worker, faster CPU than Pi 4 |
| Pi 5 (8 GB) |
8 GB |
Best Pi option for a server node |
| Pi CM4 |
4–8 GB |
Good for compact cluster builds |
Minimum for a useful single-node cluster: Pi 4 4 GB or Pi 5 4 GB.
Recommended for a 3-node cluster: 1× Pi 4/5 8 GB (server), 2× Pi 4/5 4 GB (workers).
Storage
MicroSD cards are the Pi’s Achilles heel — they wear out, are slow, and corrupt under power loss. For a cluster you care about:
- USB SSD boot: Flash the OS to a USB SSD (Samsung T7, WD My Passport SSD) and boot from USB. Pi 4 and 5 support USB boot natively. Dramatically better performance and longevity.
- NVMe via CM4 or Pi 5: The Pi 5 has a PCIe slot via the GPIO header; with an adapter board you can attach an NVMe SSD directly.
- Network storage for Longhorn: If using Longhorn for persistent volumes, local SSDs on each node are the right substrate.
Other Hardware
- Power: Each Pi 4 needs a proper 5V/3A USB-C supply. Underpowered Pis cause mysterious instability. Use official Pi power supplies or quality third-party ones.
- Networking: Wired Ethernet per node for the cluster network. Wi-Fi works but adds latency and failure modes.
- Cooling: Active cooling (a fan case) is worth it for server nodes. Pis will thermal-throttle under sustained load without it.
- Cluster case: Cases like the Turing Pi 2, Pi Tower, or DeskPi TallPi pack multiple Pis neatly with shared power distribution.
OS Preparation
Recommended OS: Raspberry Pi OS Lite (64-bit) or Ubuntu Server 24.04
1
2
3
4
5
6
7
8
9
|
# Use Raspberry Pi Imager to flash your SD card or USB SSD
# Choose: Raspberry Pi OS Lite (64-bit) for Pis
# Or: Ubuntu Server 24.04 LTS for more familiar Ubuntu tooling
# In Raspberry Pi Imager, configure:
# - Hostname: k3s-server-01 (or k3s-worker-01, etc.)
# - SSH: enabled, with your public key
# - User: pi (or your preferred username)
# - WiFi: leave unconfigured (use Ethernet)
|
Essential Pre-Installation Config
SSH into each node and run:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
|
# Update the OS
sudo apt-get update && sudo apt-get upgrade -y
# Set a static IP (edit to match your network)
sudo nmcli con modify "Wired connection 1" \
ipv4.method manual \
ipv4.addresses 192.168.1.101/24 \
ipv4.gateway 192.168.1.1 \
ipv4.dns "192.168.1.1 8.8.8.8"
sudo nmcli con up "Wired connection 1"
# Enable cgroups v2 (required for Kubernetes on Pi OS)
# Edit /boot/firmware/cmdline.txt (Pi OS) or /boot/cmdline.txt
sudo sed -i '$ s/$/ cgroup_enable=cpuset cgroup_enable=memory cgroup_memory=1/' \
/boot/firmware/cmdline.txt
# Disable swap (Kubernetes requirement)
sudo swapoff -a
sudo systemctl disable dphys-swapfile 2>/dev/null || true
sudo sed -i '/ swap / s/^/#/' /etc/fstab
# Load required kernel modules
cat <<EOF | sudo tee /etc/modules-load.d/k8s.conf
overlay
br_netfilter
EOF
sudo modprobe overlay
sudo modprobe br_netfilter
# Required sysctl params
cat <<EOF | sudo tee /etc/sysctl.d/k8s.conf
net.bridge.bridge-nf-call-iptables = 1
net.bridge.bridge-nf-call-ip6tables = 1
net.ipv4.ip_forward = 1
EOF
sudo sysctl --system
# Reboot to apply cgroup changes
sudo reboot
|
Installing K3s
Single-Node Cluster (Fastest Start)
For a single Pi that acts as both server and worker:
1
|
curl -sfL https://get.k3s.io | sh -
|
That’s it. K3s is installed, started, and enabled as a systemd service. Verify:
1
2
3
4
5
6
|
sudo kubectl get nodes
# NAME STATUS ROLES AGE VERSION
# k3s-server-01 Ready control-plane,master 60s v1.31.x+k3s1
sudo kubectl get pods -A
# All system pods should be Running within 2 minutes
|
Get the kubeconfig to use from your workstation:
1
2
3
|
sudo cat /etc/rancher/k3s/k3s.yaml
# Copy this to ~/.kube/config on your local machine
# Replace "127.0.0.1" with the Pi's actual IP
|
Multi-Node Cluster
On the server node (k3s-server-01):
1
2
3
4
5
6
7
8
9
10
|
# Install server with specific options
curl -sfL https://get.k3s.io | sh -s - server \
--cluster-init \
--disable=traefik \ # We'll install our own Traefik via Helm
--disable=servicelb \ # We'll use MetalLB instead
--write-kubeconfig-mode=644 \
--node-name=k3s-server-01 \
--advertise-address=192.168.1.101 \
--tls-san=192.168.1.101 \ # Include your server IP in TLS cert SANs
--tls-san=k3s.home.arpa # And your DNS name if you have one
|
Get the node token (needed to join workers):
1
2
|
sudo cat /var/lib/rancher/k3s/server/node-token
# K10abc123def456...
|
On each worker node:
1
2
3
4
5
6
7
|
K3S_TOKEN="K10abc123def456..." # token from server
K3S_SERVER="https://192.168.1.101:6443"
curl -sfL https://get.k3s.io | sh -s - agent \
--server=$K3S_SERVER \
--token=$K3S_TOKEN \
--node-name=k3s-worker-01
|
Verify from the server:
1
2
3
4
5
|
sudo kubectl get nodes -o wide
# NAME STATUS ROLES VERSION
# k3s-server-01 Ready control-plane,master v1.31.x+k3s1
# k3s-worker-01 Ready <none> v1.31.x+k3s1
# k3s-worker-02 Ready <none> v1.31.x+k3s1
|
Local Access from Your Workstation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
# On the server
sudo cat /etc/rancher/k3s/k3s.yaml
# Copy to your local machine, replacing server IP
mkdir -p ~/.kube
ssh pi@192.168.1.101 "sudo cat /etc/rancher/k3s/k3s.yaml" \
| sed 's/127.0.0.1/192.168.1.101/' \
> ~/.kube/config-k3s-homelab
# Use this cluster
export KUBECONFIG=~/.kube/config-k3s-homelab
# Or merge into your existing kubeconfig:
# KUBECONFIG=~/.kube/config:~/.kube/config-k3s-homelab kubectl config view --flatten > ~/.kube/config
kubectl get nodes
|
Kubernetes services of type LoadBalancer need something to hand out real IPs. In the cloud, the cloud provider does this. On homelab, you need MetalLB.
MetalLB assigns IPs from a pool you define to LoadBalancer services. Configure it to use a range in your LAN that won’t conflict with DHCP:
1
2
3
4
5
|
# Install MetalLB
kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/v0.14.5/config/manifests/metallb-native.yaml
# Wait for it to be ready
kubectl wait -n metallb-system deployment/controller --for=condition=Available --timeout=90s
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
# metallb-config.yaml
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
name: homelab-pool
namespace: metallb-system
spec:
addresses:
- 192.168.1.200-192.168.1.220 # Reserve this range in your router's DHCP exclusions
---
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
name: homelab-l2
namespace: metallb-system
spec:
ipAddressPools:
- homelab-pool
|
1
|
kubectl apply -f metallb-config.yaml
|
Traefik as Ingress Controller
K3s ships with Traefik, but we disabled it above to install a clean version via Helm with more control:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
# Add Traefik Helm repo
helm repo add traefik https://traefik.github.io/charts
helm repo update
# Install Traefik
helm install traefik traefik/traefik \
--namespace traefik \
--create-namespace \
--set deployment.replicas=1 \
--set service.type=LoadBalancer \
--set ingressClass.enabled=true \
--set ingressClass.isDefaultClass=true \
--set ports.web.redirectTo.port=websecure \
--set logs.general.level=INFO
|
Check what IP MetalLB assigned to Traefik:
1
2
|
kubectl get svc -n traefik traefik
# EXTERNAL-IP will be something like 192.168.1.200
|
Point a wildcard DNS record at this IP. In your router or Pi-hole:
*.k8s.home.arpa → 192.168.1.200
cert-manager for Automatic TLS
1
2
3
4
|
# Install cert-manager
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/latest/download/cert-manager.yaml
kubectl wait -n cert-manager deployment/cert-manager --for=condition=Available --timeout=120s
|
For local-only HTTPS with a self-signed CA (no public domain needed):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
# local-ca-issuer.yaml
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: selfsigned-issuer
spec:
selfSigned: {}
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: homelab-ca
namespace: cert-manager
spec:
isCA: true
commonName: homelab-ca
secretName: homelab-ca-secret
privateKey:
algorithm: ECDSA
size: 256
issuerRef:
name: selfsigned-issuer
kind: ClusterIssuer
---
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: homelab-ca-issuer
spec:
ca:
secretName: homelab-ca-secret
|
1
|
kubectl apply -f local-ca-issuer.yaml
|
For public TLS with Let’s Encrypt (requires a publicly accessible domain and DNS challenge or HTTP challenge):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
# letsencrypt-issuer.yaml
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: you@example.com
privateKeySecretRef:
name: letsencrypt-prod
solvers:
- http01:
ingress:
ingressClassName: traefik
|
Persistent Storage with Longhorn
By default K3s uses local-path storage — data lives on the node that runs the pod. If the pod moves to another node, data is gone. Longhorn distributes and replicates volumes across nodes.
Install Longhorn
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
# Prerequisites on each node
sudo apt-get install -y open-iscsi nfs-common
# Install via Helm
helm repo add longhorn https://charts.longhorn.io
helm repo update
helm install longhorn longhorn/longhorn \
--namespace longhorn-system \
--create-namespace \
--set defaultSettings.defaultReplicaCount=2 \ # replicate data to 2 nodes
--set defaultSettings.storageMinimalAvailablePercentage=15
# Wait for all components to be ready (takes 2-3 minutes)
kubectl -n longhorn-system rollout status deploy/longhorn-manager
|
Verify Longhorn has a storage class:
1
2
3
4
|
kubectl get storageclass
# NAME PROVISIONER
# local-path (default) rancher.io/local-path
# longhorn driver.longhorn.io
|
Make Longhorn the default:
1
2
|
kubectl patch storageclass local-path -p '{"metadata":{"annotations":{"storageclass.kubernetes.io/is-default-class":"false"}}}'
kubectl patch storageclass longhorn -p '{"metadata":{"annotations":{"storageclass.kubernetes.io/is-default-class":"true"}}}'
|
Accessing the Longhorn UI
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
# longhorn-ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: longhorn-ui
namespace: longhorn-system
annotations:
cert-manager.io/cluster-issuer: homelab-ca-issuer
spec:
ingressClassName: traefik
rules:
- host: longhorn.k8s.home.arpa
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: longhorn-frontend
port:
number: 80
tls:
- hosts:
- longhorn.k8s.home.arpa
secretName: longhorn-tls
|
1
2
|
kubectl apply -f longhorn-ingress.yaml
# Access at https://longhorn.k8s.home.arpa
|
Deploying Real Workloads
Example: Deploying Gitea
A self-hosted Git server is a good real-world test — it needs persistent storage, ingress, and a database.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
|
# gitea.yaml
apiVersion: v1
kind: Namespace
metadata:
name: gitea
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: gitea-data
namespace: gitea
spec:
accessModes: [ReadWriteOnce]
storageClassName: longhorn
resources:
requests:
storage: 10Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: gitea
namespace: gitea
spec:
replicas: 1
selector:
matchLabels:
app: gitea
template:
metadata:
labels:
app: gitea
spec:
containers:
- name: gitea
image: gitea/gitea:latest
ports:
- containerPort: 3000
name: http
- containerPort: 22
name: ssh
env:
- name: GITEA__database__DB_TYPE
value: sqlite3
- name: GITEA__server__ROOT_URL
value: https://git.k8s.home.arpa
- name: GITEA__server__SSH_DOMAIN
value: git.k8s.home.arpa
volumeMounts:
- name: data
mountPath: /data
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "512Mi"
cpu: "500m"
volumes:
- name: data
persistentVolumeClaim:
claimName: gitea-data
---
apiVersion: v1
kind: Service
metadata:
name: gitea
namespace: gitea
spec:
selector:
app: gitea
ports:
- name: http
port: 3000
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: gitea
namespace: gitea
annotations:
cert-manager.io/cluster-issuer: homelab-ca-issuer
spec:
ingressClassName: traefik
rules:
- host: git.k8s.home.arpa
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: gitea
port:
number: 3000
tls:
- hosts:
- git.k8s.home.arpa
secretName: gitea-tls
|
1
2
3
|
kubectl apply -f gitea.yaml
kubectl -n gitea rollout status deploy/gitea
# Access at https://git.k8s.home.arpa
|
Resource Management on Pi Hardware
Pi nodes have limited RAM. Set requests and limits on every workload:
1
2
3
4
5
6
7
|
resources:
requests:
memory: "64Mi" # Guaranteed allocation
cpu: "50m" # 0.05 CPU cores
limits:
memory: "256Mi" # Hard cap — pod OOMKilled if exceeded
cpu: "500m" # 0.5 CPU cores
|
Without limits, one misbehaving pod can starve the entire node. Set them consistently.
Node resource summary:
1
2
|
kubectl top nodes # Actual usage (requires metrics-server)
kubectl describe node k3s-worker-01 # Allocatable resources and current requests
|
Install metrics-server if not present:
1
2
3
4
5
6
|
kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml
# On Pi, add --kubelet-insecure-tls flag
kubectl patch deployment metrics-server -n kube-system \
--type='json' \
-p='[{"op":"add","path":"/spec/template/spec/containers/0/args/-","value":"--kubelet-insecure-tls"}]'
|
Upgrading K3s
K3s upgrades are straightforward with the system-upgrade-controller:
1
2
|
# Install the upgrade controller
kubectl apply -f https://github.com/rancher/system-upgrade-controller/releases/latest/download/system-upgrade-controller.yaml
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
# upgrade-plan.yaml
apiVersion: upgrade.cattle.io/v1
kind: Plan
metadata:
name: k3s-server
namespace: system-upgrade
spec:
concurrency: 1
cordon: true
nodeSelector:
matchExpressions:
- {key: node-role.kubernetes.io/control-plane, operator: In, values: ["true"]}
serviceAccountName: system-upgrade
upgrade:
image: rancher/k3s-upgrade
channel: https://update.k3s.io/v1-release/channels/stable
---
apiVersion: upgrade.cattle.io/v1
kind: Plan
metadata:
name: k3s-agent
namespace: system-upgrade
spec:
concurrency: 1
cordon: true
nodeSelector:
matchExpressions:
- {key: node-role.kubernetes.io/control-plane, operator: NotIn, values: ["true"]}
serviceAccountName: system-upgrade
prepare:
image: rancher/k3s-upgrade
args: ["prepare", "k3s-server"]
upgrade:
image: rancher/k3s-upgrade
channel: https://update.k3s.io/v1-release/channels/stable
|
1
2
|
kubectl apply -f upgrade-plan.yaml
# K3s will now automatically upgrade nodes when new stable releases are available
|
Backing Up the Cluster State
K3s uses SQLite (or etcd in HA mode) for cluster state. Back it up regularly:
1
2
3
4
5
6
7
8
9
|
# SQLite backup (single-node / embedded DB)
sudo k3s etcd-snapshot save --name homelab-backup
# Snapshots go to /var/lib/rancher/k3s/server/db/snapshots/
ls -lh /var/lib/rancher/k3s/server/db/snapshots/
# Copy to NAS or offsite storage
rsync -av /var/lib/rancher/k3s/server/db/snapshots/ \
nas.home.arpa:/backups/k3s/
|
As a systemd timer:
1
2
3
4
5
6
7
8
|
# /etc/systemd/system/k3s-backup.service
[Unit]
Description=K3s cluster state backup
[Service]
Type=oneshot
ExecStart=/usr/local/bin/k3s etcd-snapshot save --name daily-backup
ExecStartPost=/usr/bin/rsync -av /var/lib/rancher/k3s/server/db/snapshots/ nas.home.arpa:/backups/k3s/
|
1
2
3
4
5
6
7
8
9
10
|
# /etc/systemd/system/k3s-backup.timer
[Unit]
Description=Daily K3s backup
[Timer]
OnCalendar=daily
Persistent=true
[Install]
WantedBy=timers.target
|
1
|
sudo systemctl enable --now k3s-backup.timer
|
Troubleshooting Common Pi Issues
Node Not Joining Cluster
1
2
3
4
5
6
7
8
9
10
11
12
|
# Check agent logs on worker
sudo journalctl -u k3s-agent -f
# Common causes:
# 1. Firewall blocking port 6443 on server
sudo ufw allow from 192.168.1.0/24 to any port 6443
# 2. Token mismatch — re-verify
sudo cat /var/lib/rancher/k3s/server/node-token # on server
# 3. Time skew between nodes — sync NTP
sudo timedatectl set-ntp true
|
Pods Stuck in Pending
1
2
3
4
5
6
7
8
|
kubectl describe pod <pod-name> -n <namespace>
# Look for Events section — "0/3 nodes available" means resource pressure
# Check node resources
kubectl describe nodes | grep -A 5 "Allocated resources"
# Check if PVC is unbound (storage issue)
kubectl get pvc -A
|
High Memory Pressure
1
2
3
4
5
6
7
8
|
# Check actual memory usage per pod
kubectl top pods -A --sort-by=memory
# Check if any pods are being OOMKilled
kubectl get events -A | grep OOMKilled
# Increase swap only as an emergency measure
sudo dphys-swapfile install # NOT recommended for production
|
Longhorn Volume Degraded After Node Reboot
1
2
3
4
5
6
7
8
|
# Check Longhorn volume health
kubectl get volumes -n longhorn-system
# A 2-replica volume can survive one node being offline
# Wait for the node to come back — Longhorn auto-heals
# Force reattach if needed
kubectl -n longhorn-system get volume <volume-name> -o yaml
|
A Reference Cluster Architecture
A practical 3-node Pi homelab cluster:
┌─────────────────────────────────────────────────────────┐
│ Router / Pi-hole DNS (192.168.1.1) │
│ *.k8s.home.arpa → 192.168.1.200 (MetalLB IP) │
└────────────────────┬────────────────────────────────────┘
│ Gigabit Ethernet
┌────────────┼────────────┐
│ │ │
┌────▼────┐ ┌────▼────┐ ┌───▼─────┐
│ Pi 5 8G │ │ Pi 4 4G │ │ Pi 4 4G │
│ server │ │ worker-1│ │ worker-2│
│.1.101 │ │.1.102 │ │.1.103 │
│ USB SSD │ │ USB SSD │ │ USB SSD │
└─────────┘ └─────────┘ └─────────┘
│ │ │
Control plane Workloads Workloads
etcd/SQLite Longhorn Longhorn
Traefik LB replicas replicas
Stack running on this cluster:
- MetalLB (LoadBalancer IPs)
- Traefik (Ingress + TLS termination)
- cert-manager (automatic certificates)
- Longhorn (replicated persistent storage)
- metrics-server (resource monitoring)
- Gitea (self-hosted Git)
- Uptime Kuma (monitoring)
- Anything else you’d normally run in Docker Compose
Total hardware cost: ~$300–350 for the Pis, cases, SSDs, and power supplies. Monthly electricity: ~15–20W idle for all three nodes combined, roughly $1.50–2/month.
From Docker Compose to K3s
If you’re running services in Docker Compose on a single machine and want to migrate to K3s, the path is straightforward: each Compose service becomes a Deployment, each volume becomes a PVC, and each port mapping becomes a Service + Ingress.
A simple migration of a Compose service:
1
2
3
4
5
6
7
8
9
|
# docker-compose.yml (before)
services:
uptime-kuma:
image: louislam/uptime-kuma:latest
volumes:
- kuma-data:/app/data
ports:
- "3001:3001"
restart: unless-stopped
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
|
# k8s-uptime-kuma.yaml (after)
apiVersion: apps/v1
kind: Deployment
metadata:
name: uptime-kuma
namespace: monitoring
spec:
replicas: 1
selector:
matchLabels:
app: uptime-kuma
template:
metadata:
labels:
app: uptime-kuma
spec:
containers:
- name: uptime-kuma
image: louislam/uptime-kuma:latest
ports:
- containerPort: 3001
volumeMounts:
- name: data
mountPath: /app/data
resources:
requests: {memory: "128Mi", cpu: "100m"}
limits: {memory: "256Mi", cpu: "500m"}
volumes:
- name: data
persistentVolumeClaim:
claimName: uptime-kuma-data
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: uptime-kuma-data
namespace: monitoring
spec:
accessModes: [ReadWriteOnce]
storageClassName: longhorn
resources:
requests:
storage: 1Gi
---
apiVersion: v1
kind: Service
metadata:
name: uptime-kuma
namespace: monitoring
spec:
selector:
app: uptime-kuma
ports:
- port: 3001
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: uptime-kuma
namespace: monitoring
annotations:
cert-manager.io/cluster-issuer: homelab-ca-issuer
spec:
ingressClassName: traefik
rules:
- host: status.k8s.home.arpa
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: uptime-kuma
port:
number: 3001
tls:
- hosts: [status.k8s.home.arpa]
secretName: uptime-kuma-tls
|
The Kubernetes version is more verbose, but it gains: automatic restarts with health checks, rolling deployments with zero downtime, data replicated across nodes by Longhorn, and proper HTTPS via cert-manager. That’s the trade-off you’re making when you move from Compose to K3s — more config, more capability, more operational robustness.
Comments