You don’t need a cloud provider to run Kubernetes. You don’t need expensive servers. You need three mini-PCs and an afternoon.
This is how I built my homelab cluster — the same cluster that runs my GitLab, monitoring, home automation, and everything else I refuse to trust to someone else’s computer.
Why K3s?
K3s is Kubernetes, simplified:
- Single binary — ~70MB, includes everything
- Low resource — Runs on Raspberry Pi, runs great on mini-PCs
- Production ready — Same API, same workloads, less overhead
- Batteries included — Built-in ingress, load balancer, storage
It’s not “Kubernetes lite.” It’s Kubernetes without the enterprise cruft.
Hardware Selection
The Sweet Spot: Refurbished Mini-PCs
I recommend refurbished business mini-PCs:
| What | Why |
|---|---|
| HP EliteDesk 800 G3 | Reliable, cheap, available |
| Lenovo ThinkCentre M720q | Great thermals, quiet |
| Dell OptiPlex 3060 Micro | Good expansion options |
Target specs per node:
- CPU: Intel i5 6th-8th gen (4-6 cores)
- RAM: 16-32GB (DDR4 upgradeable)
- Storage: 256GB+ NVMe/SSD
- Network: Gigabit Ethernet
Cost: €100-200 per node. Three nodes for €300-600.
Why Not Raspberry Pi?
Pis work, but:
- ARM compatibility issues with some images
- SD card corruption is common
- Limited RAM (8GB max on Pi 4)
- USB-attached storage is finicky
Mini-PCs give you x86, NVMe, real RAM, and actual reliability.
My Cluster
Node 1: HP EliteDesk 800 G3 Mini
i5-7500T, 32GB RAM, 512GB NVMe
Role: Control plane + Worker
Node 2: HP EliteDesk 800 G3 Mini
i5-7500T, 32GB RAM, 512GB NVMe
Role: Control plane + Worker
Node 3: HP EliteDesk 800 G3 Mini
i5-7500T, 16GB RAM, 256GB NVMe
Role: Control plane + Worker
Total: ~€450, 12 cores, 80GB RAM, 1.2TB storage.
Network Setup
Static IPs or DHCP Reservations
Each node needs a predictable IP:
192.168.1.10 k3s-node-1
192.168.1.11 k3s-node-2
192.168.1.12 k3s-node-3
192.168.1.100 k3s-api # VIP for API server
DNS Entries
Add to your local DNS (or /etc/hosts if desperate):
192.168.1.10 k3s-node-1.home.lan
192.168.1.11 k3s-node-2.home.lan
192.168.1.12 k3s-node-3.home.lan
192.168.1.100 k3s.home.lan
OS Installation
Option 1: Ubuntu Server (Easiest)
Install Ubuntu Server 22.04 LTS minimal:
# After install, update
sudo apt update && sudo apt upgrade -y
# Install useful tools
sudo apt install -y curl wget vim htop
# Disable swap (required for Kubernetes)
sudo swapoff -a
sudo sed -i '/ swap / s/^/#/' /etc/fstab
Option 2: Talos Linux (Most Secure)
For a truly immutable setup, use Talos Linux. But that’s a separate topic — Ubuntu works fine for getting started.
K3s Installation
Initialize First Node (HA Mode)
# On node 1
curl -sfL https://get.k3s.io | sh -s - server \
--cluster-init \
--tls-san k3s.home.lan \
--tls-san 192.168.1.100 \
--disable traefik \
--disable servicelb
Flags explained:
--cluster-init: Enable embedded etcd for HA--tls-san: Add SANs for API access--disable traefik: We’ll install our own ingress--disable servicelb: We’ll use MetalLB instead
Get the token for joining nodes:
sudo cat /var/lib/rancher/k3s/server/node-token
Join Additional Control Plane Nodes
# On node 2 and 3
curl -sfL https://get.k3s.io | sh -s - server \
--server https://192.168.1.10:6443 \
--token <TOKEN_FROM_NODE_1> \
--tls-san k3s.home.lan \
--tls-san 192.168.1.100 \
--disable traefik \
--disable servicelb
Verify Cluster
# Get kubeconfig
sudo cat /etc/rancher/k3s/k3s.yaml
# Copy to your workstation, update server URL
export KUBECONFIG=~/.kube/k3s-config
# Check nodes
kubectl get nodes
NAME STATUS ROLES AGE VERSION
k3s-node-1 Ready control-plane,etcd,master 5m v1.28.5+k3s1
k3s-node-2 Ready control-plane,etcd,master 3m v1.28.5+k3s1
k3s-node-3 Ready control-plane,etcd,master 2m v1.28.5+k3s1
High Availability Setup
API Server Load Balancing
With 3 control plane nodes, you need a VIP or load balancer. Options:
Option A: kube-vip (Recommended)
# On each control plane node
kubectl apply -f https://kube-vip.io/manifests/rbac.yaml
# Create kube-vip manifest
cat <<EOF | sudo tee /var/lib/rancher/k3s/server/manifests/kube-vip.yaml
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: kube-vip
namespace: kube-system
spec:
selector:
matchLabels:
app: kube-vip
template:
metadata:
labels:
app: kube-vip
spec:
hostNetwork: true
tolerations:
- key: node-role.kubernetes.io/control-plane
operator: Exists
effect: NoSchedule
containers:
- name: kube-vip
image: ghcr.io/kube-vip/kube-vip:latest
args:
- manager
env:
- name: vip_interface
value: eth0
- name: vip_address
value: 192.168.1.100
- name: vip_arp
value: "true"
- name: lb_enable
value: "true"
EOF
Option B: HAProxy on Router
If your router supports it, configure HAProxy:
frontend k3s-api
bind *:6443
default_backend k3s-servers
backend k3s-servers
balance roundrobin
server node1 192.168.1.10:6443 check
server node2 192.168.1.11:6443 check
server node3 192.168.1.12:6443 check
Essential Components
MetalLB for Load Balancer Services
kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/v0.14.3/config/manifests/metallb-native.yaml
Configure IP pool:
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
name: default
namespace: metallb-system
spec:
addresses:
- 192.168.1.200-192.168.1.250
---
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
name: default
namespace: metallb-system
Ingress Controller (Traefik or Nginx)
# Traefik (via Helm)
helm repo add traefik https://traefik.github.io/charts
helm install traefik traefik/traefik \
--namespace traefik \
--create-namespace \
--set service.type=LoadBalancer
Storage (Longhorn)
helm repo add longhorn https://charts.longhorn.io
helm install longhorn longhorn/longhorn \
--namespace longhorn-system \
--create-namespace
See my Longhorn vs Rook-Ceph comparison for more.
Kubeconfig for Remote Access
Copy kubeconfig to your workstation:
# On node 1
sudo cat /etc/rancher/k3s/k3s.yaml
# Save locally, edit server URL
# Change: server: https://127.0.0.1:6443
# To: server: https://k3s.home.lan:6443
# Set permissions
chmod 600 ~/.kube/k3s-config
export KUBECONFIG=~/.kube/k3s-config
Maintenance
Upgrading K3s
# On each node, one at a time
curl -sfL https://get.k3s.io | sh -s - server \
# same flags as initial install
Or use the system-upgrade-controller:
kubectl apply -f https://github.com/rancher/system-upgrade-controller/releases/latest/download/system-upgrade-controller.yaml
Backup etcd
# Manual snapshot
k3s etcd-snapshot save --name manual-backup
# Scheduled backups (add to k3s config)
# /etc/rancher/k3s/config.yaml
etcd-snapshot-schedule-cron: "0 */6 * * *"
etcd-snapshot-retention: 5
Node Maintenance
# Drain node before maintenance
kubectl drain k3s-node-2 --ignore-daemonsets --delete-emptydir-data
# Do maintenance...
# Uncordon when done
kubectl uncordon k3s-node-2
Common Issues
“Unable to connect to server”
Check:
- K3s service running:
sudo systemctl status k3s - Firewall allows 6443:
sudo ufw status - kubeconfig has correct server URL
Node NotReady
Check:
- Kubelet logs:
sudo journalctl -u k3s -f - Container runtime:
sudo crictl ps - Disk space:
df -h
etcd Issues
# Check etcd health
sudo k3s etcd-snapshot list
# Force new cluster if etcd corrupted (DANGEROUS)
# sudo k3s server --cluster-reset
What to Run
Once your cluster is up:
- GitLab for code hosting
- Prometheus/Thanos for monitoring
- ArgoCD for GitOps
- Home Assistant for automation
- Nextcloud for file sync
- Vaultwarden for passwords
Everything self-hosted, everything under your control.
Cost Comparison
| Cloud (3 nodes) | Homelab (3 nodes) | |
|---|---|---|
| Monthly | €150-300 | €10-20 (electricity) |
| Annual | €1800-3600 | €120-240 |
| 3 year | €5400-10800 | €360-720 + €450 hardware |
Homelab pays for itself in 3-6 months.
Why This Matters
Running your own cluster teaches you:
- How Kubernetes actually works (not just how to use it)
- What happens when components fail
- Real capacity planning
And you get sovereignty over your infrastructure. Your data, your rules, your responsibility.
Cloud is fine for work. Home is for things that matter.
The best way to understand Kubernetes is to break it. The best place to break it is your homelab.
