Skip to content
Go back

k3s Cluster on 3 Mini PCs From Zero

By SumGuy 8 min read
k3s Cluster on 3 Mini PCs From Zero

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:

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:

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):

/etc/netplan/01-config.yaml
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.1

Apply it:

Terminal window
sudo netplan apply

Verify:

Terminal window
ip addr show eth0

Do 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:

Terminal window
sudo apt-get update && sudo apt-get upgrade -y
sudo apt-get install -y curl wget git htop

Install k3s on the control plane:

Terminal window
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:

Terminal window
sudo k3s kubectl get nodes

You’ll see something like:

NAME STATUS ROLES AGE VERSION
node-1 Ready control-plane,master 2m v1.29.0+k3s1

Now grab your kubeconfig so you can run kubectl from your laptop:

Terminal window
sudo cat /etc/rancher/k3s/k3s.yaml

Copy the entire output. On your laptop, save it:

Terminal window
# on your laptop
mkdir -p ~/.kube
cat > ~/.kube/config << 'EOF'
# paste the k3s.yaml content here
EOF

Edit that file and replace 127.0.0.1 with your actual control plane IP:

Terminal window
# In ~/.kube/config, change:
# server: https://127.0.0.1:6443
# To:
# server: https://192.168.1.100:6443

Test from your laptop:

Terminal window
kubectl get nodes

If 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:

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

On node-2, install k3s with the token:

Terminal window
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:

Terminal window
kubectl get nodes -o wide

Output:

NAME STATUS ROLES AGE VERSION INTERNAL-IP
node-1 Ready control-plane,master 10m v1.29.0+k3s1 192.168.1.100
node-2 Ready <none> 2m v1.29.0+k3s1 192.168.1.101
node-3 Ready <none> 1m v1.29.0+k3s1 192.168.1.102

Congrats. 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:

Terminal window
kubectl get storageclasses

You should see local-path already there. Good.

Create a test PersistentVolumeClaim:

test-pvc.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: test-storage
spec:
accessModes:
- ReadWriteOnce
storageClassName: local-path
resources:
requests:
storage: 5Gi

Apply it:

Terminal window
kubectl apply -f test-pvc.yaml
kubectl get pvc

You should see:

NAME STATUS VOLUME CAPACITY
test-storage Bound pvc-abc123... 5Gi

Good. 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.

nginx-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-demo
spec:
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: v1
kind: Service
metadata:
name: nginx-demo
spec:
type: LoadBalancer
selector:
app: nginx-demo
ports:
- protocol: TCP
port: 80
targetPort: 80

Deploy it:

Terminal window
kubectl apply -f nginx-deployment.yaml

Watch it come up:

Terminal window
kubectl get pods -w

After a minute or so, you should see three pods across your nodes:

NAME READY STATUS RESTARTS AGE NODE
nginx-demo-6c4d8f5fbc-abc1 1/1 Running 0 45s node-2
nginx-demo-6c4d8f5fbc-def2 1/1 Running 0 40s node-1
nginx-demo-6c4d8f5fbc-ghi3 1/1 Running 0 35s node-3

Check the service:

Terminal window
kubectl get svc nginx-demo

You’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/TCP

That 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:

Terminal window
curl 192.168.1.100

You 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:

Terminal window
# Forward 6443 on your laptop to the control plane
ssh -L 6443:localhost:6443 [email protected]

Then 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:

Terminal window
kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml

Wait 30 seconds, then:

Terminal window
kubectl top nodes

Output:

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?

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.


Share this post on:

Send a Webmention

Written about this post on your own site? Send a webmention and it'll show up above once verified.


Next Post
iperf3 + nload: Network Diagnosis

Discussion

Powered by Garrul . Sign in with GitHub or Google, or post anonymously.

Related Posts