You’re About to Run Kubernetes at Home (Buckle Up)
Here’s the thing: Kubernetes feels like hiring a forklift to move a couch. Technically it works, but your neighbors will have questions. Except k3s? k3s is the smart move. It’s Kubernetes that fits on actual hardware, and today we’re building a three-node cluster from bare metal.
This isn’t a “deploy to the cloud” tutorial. We’re taking three mini PCs, connecting them with Ethernet, installing k3s, wiring up persistent storage, and deploying a pod by the end. No managed control plane. No hand-waving. Just you, three boxes, and the weight of responsibility that comes with running your own infrastructure.
Let’s go.
Why Three Nodes (And Why Mini PCs)
A single-node cluster is technically Kubernetes, but it’s like driving a car with a flat tire—technically still driving. Three nodes give you:
- High availability — if one node dies, your pods migrate to the others
- Storage redundancy — persistent storage can replicate across nodes
- Resource spreading — you’re not betting everything on one machine
Mini PCs (think Beelink, ASUS PN50, old ThinkCentres) are perfect because they’re cheap (£200–400 each), quiet, and efficient. No fan noise at 2 AM, no power bill that makes you weep. And if one catches fire, you’ve only lost £300, not £3,000.
Before You Start: CIDR Planning
Here’s the trap most people fall into: they wing it. IP addresses “just happen,” overlaps occur, and suddenly your pods can’t talk to anything. Don’t be that person.
Set this down on paper:
- Cluster CIDR (pods):
10.42.0.0/16— k3s default, you can keep it - Service CIDR (services):
10.43.0.0/16— k3s default, keep it - Host network:
192.168.1.0/24— your home lab (you probably already have this) - Node IPs (static):
192.168.1.100— node-1 (will be the control plane)192.168.1.101— node-2192.168.1.102— node-3
Why static? Because k3s will give pods stable DNS names, but if your nodes’ IPs float around, you’ll lose your mind troubleshooting.
Set static IPs on each mini PC now (assuming Ubuntu 24.04 LTS — adjust for your distro):
network: version: 2 ethernets: eth0: dhcp4: no addresses: - 192.168.1.100/24 # change per node: .101, .102 nameservers: addresses: [8.8.8.8, 8.8.4.4] routes: - to: 0.0.0.0/0 via: 192.168.1.1Apply it:
sudo netplan applyVerify:
ip addr show eth0Do this on all three machines. Your 2 AM self will appreciate it.
Installing k3s: The Control Plane (Node-1)
SSH into your first mini PC (the one at 192.168.1.100). This will be your control plane.
Update and add basics:
sudo apt-get update && sudo apt-get upgrade -ysudo apt-get install -y curl wget git htopInstall k3s on the control plane:
curl -sfL https://get.k3s.io | sh -That’s it. Wait 30–60 seconds. k3s installs itself, bootstraps etcd, and brings up the control plane.
Verify the control plane is up:
sudo k3s kubectl get nodesYou’ll see something like:
NAME STATUS ROLES AGE VERSIONnode-1 Ready control-plane,master 2m v1.29.0+k3s1Now grab your kubeconfig so you can run kubectl from your laptop:
sudo cat /etc/rancher/k3s/k3s.yamlCopy the entire output. On your laptop, save it:
# on your laptopmkdir -p ~/.kubecat > ~/.kube/config << 'EOF'# paste the k3s.yaml content hereEOFEdit that file and replace 127.0.0.1 with your actual control plane IP:
# In ~/.kube/config, change:# server: https://127.0.0.1:6443# To:# server: https://192.168.1.100:6443Test from your laptop:
kubectl get nodesIf you see node-1 Ready, you’re golden. If not, check your firewall (port 6443 needs to be open between your laptop and the control plane).
Adding Workers (Nodes 2 & 3)
SSH into node-2. Install k3s, but this time as an agent pointing to the control plane:
First, grab the node token from the control plane. On node-1:
sudo cat /var/lib/rancher/k3s/server/node-tokenOn node-2, install k3s with the token:
curl -sfL https://get.k3s.io | K3S_URL=https://192.168.1.100:6443 K3S_TOKEN=<paste-token-here> sh -Wait 30 seconds. Repeat for node-3.
Verify all three nodes are registered:
kubectl get nodes -o wideOutput:
NAME STATUS ROLES AGE VERSION INTERNAL-IPnode-1 Ready control-plane,master 10m v1.29.0+k3s1 192.168.1.100node-2 Ready <none> 2m v1.29.0+k3s1 192.168.1.101node-3 Ready <none> 1m v1.29.0+k3s1 192.168.1.102Congrats. You now have a Kubernetes cluster. The hard part? Actually done. The interesting part is next.
Persistent Storage: Making Data Stick Around
Kubernetes doesn’t care about your data by default. Pods die, and poof—your stuff is gone. We need persistent storage.
k3s comes with a local-path provisioner out of the box, which is great for homelab. It creates storage on the node’s local filesystem. For a three-node cluster, this is enough, but it’s not replicated—lose the node, lose the data.
For now, let’s use it. Later, you can wire up Longhorn for real redundancy. But let’s walk before we run.
Check what’s available:
kubectl get storageclassesYou should see local-path already there. Good.
Create a test PersistentVolumeClaim:
apiVersion: v1kind: PersistentVolumeClaimmetadata: name: test-storagespec: accessModes: - ReadWriteOnce storageClassName: local-path resources: requests: storage: 5GiApply it:
kubectl apply -f test-pvc.yamlkubectl get pvcYou should see:
NAME STATUS VOLUME CAPACITYtest-storage Bound pvc-abc123... 5GiGood. Your storage pipeline works. We’ll use this later.
Your First Real Deployment: Nginx
Alright, let’s deploy something. Pick the most boring workload: an Nginx web server. If you can’t deploy Nginx, nothing else works.
apiVersion: apps/v1kind: Deploymentmetadata: name: nginx-demospec: replicas: 3 selector: matchLabels: app: nginx-demo template: metadata: labels: app: nginx-demo spec: containers: - name: nginx image: nginx:alpine ports: - containerPort: 80 resources: requests: memory: "64Mi" cpu: "100m" limits: memory: "128Mi" cpu: "200m"---apiVersion: v1kind: Servicemetadata: name: nginx-demospec: type: LoadBalancer selector: app: nginx-demo ports: - protocol: TCP port: 80 targetPort: 80Deploy it:
kubectl apply -f nginx-deployment.yamlWatch it come up:
kubectl get pods -wAfter a minute or so, you should see three pods across your nodes:
NAME READY STATUS RESTARTS AGE NODEnginx-demo-6c4d8f5fbc-abc1 1/1 Running 0 45s node-2nginx-demo-6c4d8f5fbc-def2 1/1 Running 0 40s node-1nginx-demo-6c4d8f5fbc-ghi3 1/1 Running 0 35s node-3Check the service:
kubectl get svc nginx-demoYou’ll see something like:
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S)nginx-demo LoadBalancer 10.43.12.34 192.168.1.100 80:31234/TCPThat EXTERNAL-IP is the IP of one of your nodes (k3s uses ServiceLB/klipper-lb to assign a LoadBalancer IP from your host network). Hit it:
curl 192.168.1.100You should get the Nginx welcome page. You’ve just deployed a real application to your Kubernetes cluster.
Accessing kubeconfig Remotely (And Safely)
Right now, you need kubectl on your laptop and a valid kubeconfig. If you’re traveling or on a different network, SSH tunneling saves you:
# Forward 6443 on your laptop to the control planeThen point kubectl at https://localhost:6443 (adjust your kubeconfig accordingly).
Better yet: set up WireGuard on your home network so you can access 192.168.1.x from anywhere. (That’s a separate article, but worth doing now.)
Monitoring: Is My Cluster Healthy?
Install metrics-server so you can see resource usage:
kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yamlWait 30 seconds, then:
kubectl top nodesOutput:
NAME CPU(cores) CPU% MEMORY(Mi) MEMORY%node-1 250m 12% 512Mi 64%node-2 120m 6% 384Mi 48%node-3 150m 7% 456Mi 57%Great. You can see what’s actually running. Use this whenever you wonder if your cluster is on fire.
For deeper visibility, install Prometheus and Grafana later. For now, metrics-server is enough.
Common Gotchas
“Pods are stuck in ImagePullBackOff” — Your nodes can’t reach Docker Hub (or you hit rate limits). Pull images from your laptop and load them manually, or set up a private registry.
“My service IP works inside the cluster but not outside” — k3s uses kube-vip by default to assign a floating IP from your host network. If it’s not working, check that the IP range in your service annotation doesn’t conflict with your home network.
“Nodes keep leaving the cluster” — Usually a time sync issue. Run sudo timedatectl on each node and make sure they’re synced.
“PersistentVolumes are all on one node” — Yes, that’s local-path for you. It doesn’t replicate. If you care about data, install Longhorn (next level up) or use networked storage (NFS, iSCSI).
The Path Forward
You’ve got a working three-node cluster. What’s next?
- Ingress Controller: Wire up Traefik (comes with k3s) to route traffic by hostname instead of IP.
- Persistent Storage: Install Longhorn for replicated block storage—game changer for homelab.
- Monitoring: Prometheus + Grafana. You’ll want to see what’s running.
- GitOps: ArgoCD lets you define desired state in Git and have the cluster reconcile itself.
- Secrets Management: Sealed Secrets or Vault to encrypt sensitive config.
But you don’t need those today. You’ve got a functioning cluster. Deploy stuff. Break it. Learn.
This is how actual Kubernetes works—not the managed cloud version, but the real deal. Three boxes. Network cables. Logs you can actually read. And the sweet realization that you’re not paying cloud providers anymore.
Your 2 AM self thanks you already.