· · 11 min leestijd Intermediate

K3s cluster setup op refurbished mini-PCs

Bouw een production-ready K3s Kubernetes cluster op refurbished mini-PCs. Complete gids van hardware selectie tot HA setup met embedded etcd.

Drie mini-pc's op een plank doen wat het monsterrack met prijskaartje erachter ook doet

TL;DR Drie refurbished mini-PCs, K3s met embedded etcd, kube-vip voor één API-adres dat een dode node overleeft, en daarbovenop MetalLB, Traefik en Longhorn. Zo’n €450 aan hardware en een middag. Hieronder staan de commando’s, in de volgorde waarin ik ze heb gedraaid.

Drie mini-PCs staan op een plank bij mij thuis. Samen draaien ze m’n GitLab, m’n monitoring-stack, home automation, file sync, een wachtwoordmanager en nog een stapel dingen die ik liever niet op andermans computer zet. De hele setup kostte minder dan één maand van de managed-Kubernetes-rekening voor hetzelfde, en ik weet wat elke laag doet omdat ik hem er zelf in heb gezet.

Noot toegevoegd op 2026-09-23. De git-server op die plank is inmiddels Forgejo en geen GitLab. Aan het bouwen van het cluster verandert niets, dus vervang GitLab overal in deze post door wat jij voor source code management draait.

Een cloud-account of een rack heb je hier niet voor nodig. Drie tweedehands machines en een middag waarop niemand je stoort, dat is genoeg. Dit is het cluster waar ik steeds naar wijs als ik over sovereign infrastructuur schrijf, dus hier is hoe het in elkaar zit, stap voor stap.

Eerst één commando

Voordat het over hardware gaat, eerst het deel dat mensen verrast. Een werkend Kubernetes-cluster is één commando.

curl -sfL https://get.k3s.io | sh -

Dat is een K3s-installatie op één node. Een echte Kubernetes API, een echte kubectl, workloads die draaien. Wil je alleen wat met manifests prutsen op een oude laptop, dan kun je hier gewoon stoppen met lezen. Je hebt al iets bruikbaars.

K3s is de reden dat dit zo pijnloos is. Het is de volledige Kubernetes API in één binary van zo’n 70MB, met de onderdelen die de meeste homelabs nooit aanraken eruit en verstandige defaults voor de rest. Het draait op een Raspberry Pi, en op een mini-PC draait het heel goed. “Kubernetes lite” noemen mensen het wel eens, en dat doet het tekort. Wat je erop deployt is identiek aan wat je op een groot managed cluster zou deployen. Je slaat alleen de enterprise-overhead over die je toch nooit ging gebruiken.

De rest van deze post maakt van dat ene commando iets waar ik data aan toevertrouw waar ik om geef. Drie nodes, embedded etcd, een virtueel IP dat een stervende node overleeft, en de storage en ingress om er dingen op te hosten.

Waarom refurbished mini-PCs

Je kunt dit op van alles bouwen. Ik raad toch steeds hetzelfde aan, omdat het steeds weer klopt. Refurbished zakelijke mini-PCs zijn voor dit werk de beste deal die ik ken, en voor het geld komt er niks in de buurt.

WatWaarom
HP EliteDesk 800 G3Betrouwbaar, goedkoop, overal beschikbaar
Lenovo ThinkCentre M720qGoede thermals, stil
Dell OptiPlex 3060 MicroGoede uitbreidingsopties

Mik ongeveer op dit per node:

  • CPU: Intel i5 6e-8e gen (4-6 cores)
  • RAM: 16-32GB (DDR4, upgradeable)
  • Storage: 256GB+ NVMe/SSD
  • Netwerk: Gigabit Ethernet

Reken op zo’n €100-200 per node, dus €300-600 voor drie.

Naar een Raspberry Pi-cluster vraagt iedereen, en Pi’s werken op zich. Ik heb er een tijd één gedraaid. Maar je bent de hele tijd tegen het platform aan het vechten. ARM-images die nog niet bestaan voor het ding dat je wilt. SD-kaarten die zichzelf corrupt maken als een node stroom verliest. 8GB RAM als harde grens op een Pi 4. USB-storage die afhaakt op het slechtst denkbare moment. Een refurbished mini-PC geeft je x86, een NVMe-slot, fatsoenlijk RAM en een machine die gebouwd is om jarenlang zakelijke workloads te draaien zonder te mopperen. Een Pi als node heb ik sindsdien niet meer gehad.

M’n eigen cluster is drie keer dezelfde doos, dat houdt het simpel:

Node 1: HP EliteDesk 800 G3 Mini
        i5-7500T, 32GB RAM, 512GB NVMe
        Rol: Control plane + Worker

Node 2: HP EliteDesk 800 G3 Mini
        i5-7500T, 32GB RAM, 512GB NVMe
        Rol: Control plane + Worker

Node 3: HP EliteDesk 800 G3 Mini
        i5-7500T, 16GB RAM, 256GB NVMe
        Rol: Control plane + Worker

Dat is zo’n €450 voor 12 cores, 80GB RAM en 1.2TB storage.

De nodes klaarzetten

Twee dingen regelen voordat K3s erop gaat. Adressen en het OS.

Voorspelbare adressen

Elke node heeft een IP nodig dat niet beweegt. Statische IP’s, of makkelijker, DHCP-reserveringen vanaf je router.

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 voor API server

Die .100 is een virtueel IP dat we straks opzetten, zodat de API één vast adres heeft, ook als er een node sterft. Geef de nodes ook DNS-namen, in je lokale resolver, of in /etc/hosts als je haast hebt.

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

Het OS

Om te beginnen is Ubuntu Server 22.04 LTS minimal de weg van de minste weerstand. Installeren, updaten en swap uitzetten, want Kubernetes wil dat uit.

# Na installatie, update
sudo apt update && sudo apt upgrade -y

# Installeer handige tools
sudo apt install -y curl wget vim htop

# Disable swap (vereist voor Kubernetes)
sudo swapoff -a
sudo sed -i '/ swap / s/^/#/' /etc/fstab

Wil je de immutable, dichtgetimmerde versie hiervan, dan stuur ik je daarna naar Talos Linux. Het is de betere fundering en lastiger om op te beginnen, dus dat bewaar ik voor ronde twee. Met Ubuntu heb je vandaag nog een cluster.

Drie servers, één control plane

Dit is de stap van “Kubernetes op een laptop” naar “Kubernetes dat ik vertrouw”. De flag die ertoe doet is --cluster-init. Die zet embedded etcd aan, zodat de control plane het overleeft als er een node uitvalt.

Op node 1:

# Op 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

Wat die flags doen.

  • --cluster-init zet embedded etcd aan, en dat is wat de control plane HA maakt.
  • --tls-san zet de DNS-naam en het VIP als SAN in het API-certificaat, zodat het geldig is voor het adres dat we zo gaan gebruiken.
  • --disable traefik slaat de meegeleverde ingress over. Die nemen we zelf mee.
  • --disable servicelb slaat de meegeleverde load balancer over. MetalLB doet dat beter.

Pak het join-token:

sudo cat /var/lib/rancher/k3s/server/node-token

Breng dan node 2 en 3 omhoog, gericht op node 1 en met dezelfde flags:

# Op node 2 en 3
curl -sfL https://get.k3s.io | sh -s - server \
  --server https://192.168.1.10:6443 \
  --token <TOKEN_VAN_NODE_1> \
  --tls-san k3s.home.lan \
  --tls-san 192.168.1.100 \
  --disable traefik \
  --disable servicelb

Haal de kubeconfig op, zet hem op je workstation en check dat alle drie de nodes erbij zijn:

# Haal kubeconfig op
sudo cat /etc/rancher/k3s/k3s.yaml

# Kopieer naar je 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

Drie control-plane nodes, elk met een etcd-member. Nu kan er één omvallen en het cluster draait gewoon door. Daar zijn die drie dozen voor.

Eén adres dat een dode node overleeft

Er zit nog een gat in. Je kubeconfig wijst naar het IP van één node, en is dát de node die sterft, dan doet kubectl het niet meer terwijl het cluster prima draait. Je wilt één API-adres dat meebeweegt naar welke control-plane node er nog leeft.

Mijn keuze is kube-vip. Het draait als DaemonSet op de control-plane nodes en beantwoordt het VIP (192.168.1.100) via ARP, en verhuist het uit zichzelf naar een gezonde node.

# Op elke control plane node
kubectl apply -f https://kube-vip.io/manifests/rbac.yaml

# Maak 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

Wil je de VIP-logica liever buiten het cluster houden, en draait je router HAProxy, dan kun je de API daar balancen:

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

Allebei werken ze. Ik draai kube-vip, omdat alles dan binnen het cluster blijft en er één doos minder in het failure-pad zit. HAProxy op de router is makkelijker te overzien als je je router toch al vertrouwt. Kies maar wat past bij wat je al draait.

Load balancing, ingress en storage

Een kaal cluster host nog niks. Drie onderdelen maken er iets van waar je naartoe kunt deployen.

LoadBalancer-services met MetalLB

We hebben de meegeleverde servicelb uitgezet, dus een LoadBalancer-service heeft niks dat hem beantwoordt. MetalLB vult dat gat door IP’s uit te delen uit een pool op je LAN.

kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/v0.14.3/config/manifests/metallb-native.yaml

Geef het een range om uit te delen, ruim weg van je node- en VIP-adressen:

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

De meegeleverde Traefik hebben we laten vallen zodat we hem op onze eigen voorwaarden kunnen installeren. Hier is hij via Helm , met een LoadBalancer-IP uit de MetalLB-pool die we net hebben gedefinieerd:

# 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 met Longhorn

Pods die data bewaren hebben echte persistent volumes nodig. Longhorn geeft je block storage die over de drie nodes gerepliceerd is, zodat een volume het overleeft als er een node omvalt:

helm repo add longhorn https://charts.longhorn.io
helm install longhorn longhorn/longhorn \
  --namespace longhorn-system \
  --create-namespace

Twijfel je over je storage-opties, dan zet ik Longhorn naast het zwaardere alternatief in Longhorn vs Rook-Ceph.

Met het cluster praten vanaf je bureau

Laatste stukje setup. Zet een kubeconfig op je workstation, zodat je niet meer voor alles in node 1 hoeft te SSH’en. Die K3s genereert wijst naar 127.0.0.1. Ruil dat in voor de DNS-naam van je VIP en de kubeconfig blijft werken, welke node er ook antwoordt.

# Op node 1
sudo cat /etc/rancher/k3s/k3s.yaml

# Sla lokaal op, edit server URL
# Verander: server: https://127.0.0.1:6443
# Naar:     server: https://k3s.home.lan:6443

# Set permissions
chmod 600 ~/.kube/k3s-config
export KUBECONFIG=~/.kube/k3s-config

In leven houden

Een homelab-cluster dat je nooit onderhoudt is een probleem dat wacht op de dag dat het breekt. Een paar gewoontes houden het saai, en saai is wat ik van infrastructuur wil.

Upgrades

Upgrade één node tegelijk door de install opnieuw te draaien met dezelfde flags, of laat de system-upgrade-controller het voor je doen:

# Op elke node, één voor één
curl -sfL https://get.k3s.io | sh -s - server \
  # zelfde flags als initiële install
kubectl apply -f https://github.com/rancher/system-upgrade-controller/releases/latest/download/system-upgrade-controller.yaml

Backups

etcd is waar de hele cluster-state leeft. Snapshot het. K3s doet dat op een schema als je erom vraagt, dus vraag erom:

# Handmatige snapshot
k3s etcd-snapshot save --name manual-backup

# Geplande backups (voeg toe aan k3s config)
# /etc/rancher/k3s/config.yaml
etcd-snapshot-schedule-cron: "0 */6 * * *"
etcd-snapshot-retention: 5

Een node uit roulatie halen voor onderhoud

Moet er een doos open of een reboot? Drain hem eerst, zodat workloads netjes herschikken, en breng hem daarna terug:

# Drain node voor onderhoud
kubectl drain k3s-node-2 --ignore-daemonsets --delete-emptydir-data

# Doe onderhoud...

# Uncordon als klaar
kubectl uncordon k3s-node-2

Als er iets stuk is

Een paar storingen komen vaak genoeg voor dat ik ze uit m’n hoofd ken. Kan kubectl de server niet bereiken, check dan of K3s überhaupt draait, of de firewall poort 6443 niet opvreet, en of je kubeconfig wijst waar je denkt dat hij wijst:

sudo systemctl status k3s
sudo ufw status

Een node die op NotReady blijft hangen betekent meestal dat de kubelet, de container runtime of de disk niet gelukkig is:

sudo journalctl -u k3s -f
sudo crictl ps
df -h

En raakt etcd zelf corrupt, dan grijp je naar die snapshots. Het --cluster-reset-pad bestaat, maar behandel het als laatste redmiddel, want het breekt het cluster af tot één member:

# Check etcd health
sudo k3s etcd-snapshot list

# Forceer nieuw cluster als etcd corrupt (GEVAARLIJK)
# sudo k3s server --cluster-reset

Waar het voor is

Dat ene commando van bovenaan is nu drie nodes met een gedeelde HA control plane, een VIP dat een dode node overleeft, load balancing, ingress, gerepliceerde storage en een backup-schema. Genoeg om dingen op te hosten.

Dit draai ik op de mijne:

  • GitLab voor code hosting
  • Prometheus en Thanos voor monitoring
  • ArgoCD voor GitOps
  • Home Assistant voor automation
  • Nextcloud voor file sync
  • Vaultwarden voor wachtwoorden

Alles zelf gehost, alles bij mij thuis.

De geldkant is scheef. Hetzelfde in de cloud zou me €150-300 per maand kosten. De plank kost misschien €10-20 aan stroom:

Cloud (3 nodes)Homelab (3 nodes)
Maandelijks€150-300€10-20 (elektriciteit)
Jaarlijks€1800-3600€120-240
3 jaar€5400-10800€360-720 + €450 hardware

De hardware heeft zichzelf in drie tot zes maanden terugverdiend. Daarna is het compute dat van mij is, voor de prijs van de stroom.

Maar om het geld ging het me eigenlijk nooit. Je eigen cluster draaien leert je hoe Kubernetes zich gedraagt. Een managed cluster leert je welk commando het dashboard groen maakt. Je ziet wat er gebeurt als een node sterft, want dat gaat een keer gebeuren, en dan sta jij naar het VIP te kijken dat verhuist. Capacity planning leer je met je eigen RAM op het spel. Bij het control panel van een managed cluster zit dat er niet bij.

En je krijgt sovereignty over de hele stack. Je data blijft van jou, en de verantwoordelijkheid ook. De cloud is prima voor werk dat van iemand anders is. De dingen die er voor mij toe doen staan op een plank bij mij thuis, waar ik ze kan aanraken, en waar ik zo nu en dan de stekker uit één van de drie trek om te zien hoe de andere twee doordraaien.