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:

WhatWhy
HP EliteDesk 800 G3Reliable, cheap, available
Lenovo ThinkCentre M720qGreat thermals, quiet
Dell OptiPlex 3060 MicroGood 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:

  1. K3s service running: sudo systemctl status k3s
  2. Firewall allows 6443: sudo ufw status
  3. kubeconfig has correct server URL

Node NotReady

Check:

  1. Kubelet logs: sudo journalctl -u k3s -f
  2. Container runtime: sudo crictl ps
  3. 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.