Kubernetes on Your Desk: Building a Real Cluster with Two Machines
April 9, 202611 min read
Technical
Most Kubernetes tutorials hand you a single-node playground (Minikube, Kind, Docker Desktop) and call it a day. You type kubectl get pods, see a green checkmark, and learn almost nothing about what Kubernetes actually does.This guide is different. You'll build a real, multi-node, cross-architecture cluster using two physical machines you already own. By the end, you'll understand not just how to run Kubernetes, but why it works the way it does.
What You'll Need
Machine
Role
Architecture
OS
A desktop or laptop running Linux
Control plane + worker
amd64 (Intel/AMD)
Ubuntu 22.04+ recommended
A second machine (Mac, another Linux box, Raspberry Pi)
Worker node
Any (amd64 or arm64)
macOS, Ubuntu, Raspberry Pi OS
Both machines must be on the same local network (same Wi-Fi or connected to the same router).Our setup: An Ubuntu desktop (Ryzen 5 5600X, 16GB RAM) as the control plane, and a MacBook Pro (Apple M4 Pro, 24GB RAM) as a worker node. The fact that these are different CPU architectures (amd64 vs arm64) makes things more interesting and more realistic. Real-world clusters often mix architectures.Time required: 2-3 hours for a working cluster. Another hour to deploy workloads and understand what's happening.
Part 0: What Is Kubernetes, and Why Should You Care?
Kubernetes (K8s) is a system that manages containers across multiple machines. That's it. Everything else is detail.But let's unpack what that actually means, because "manages containers across multiple machines" is doing a lot of heavy lifting.
Containers: The 30-Second Version
A container is a way to package an application with everything it needs to run (code, libraries, configuration) into a single unit that runs the same way everywhere. If you've used Docker, you've used containers.The problem: one machine running containers is fine. Ten machines running containers is chaos. Which machine has capacity? What happens when one crashes? How do containers on different machines talk to each other?
What Kubernetes Solves
Kubernetes is the answer to "I have containers and more than one machine." It handles:
Scheduling: You say "run 4 copies of my web app." Kubernetes decides which machines to put them on, based on available CPU, memory, and your rules.
Self-healing: A container crashes? Kubernetes restarts it. A whole machine dies? Kubernetes moves the containers to surviving machines.
Networking: Every container gets an IP address. Containers on different machines can talk to each other as if they're on the same network.
Scaling: Traffic spike? Tell Kubernetes to run more copies. Traffic drops? Scale back down.
Rolling updates: Deploy a new version of your app without downtime. Kubernetes replaces containers one by one.
Why Two Machines Matters
On a single machine, you never see Kubernetes do the interesting work. Scheduling decisions are trivial when there's only one option. Networking between nodes doesn't exist. Failover is impossible.With two machines, you'll see:
Pods (containers) get scheduled across different physical hardware
Network traffic flows between nodes transparently
You can drain a node (take it offline) and watch workloads migrate
This is what Kubernetes was built for. One node is a toy. Two nodes is a cluster.
The Architecture in Plain English
Every Kubernetes cluster has two types of machines:Control plane (the brain):
Runs the API server, the single front door for all commands
Runs the scheduler, which decides where containers go
Runs the controller manager, which watches for things that need fixing
Stores all cluster state in etcd (a distributed database)
Worker nodes (the muscle):
Run your actual containers
Report their health back to the control plane
Run a component called kubelet that takes orders from the API server
In our setup, the Ubuntu PC will be the control plane (and also run containers; it does double duty). The Mac will be a pure worker node.
Part 1: Connect Your Machines
Before any Kubernetes setup, your two machines need to talk to each other. We'll use SSH (Secure Shell) to control the Linux machine from the Mac.
Step 1.1: Install SSH Server on the Linux Machine
On your Ubuntu machine, open a terminal and run:
sudo apt install openssh-server -y
This installs the OpenSSH server, which listens for incoming SSH connections on port 22. The -y flag auto-confirms the installation.Verify it's running:
This prints all IP addresses assigned to the machine. The first one is usually your local network IP, something like 192.168.x.x or 10.0.x.x.Write this IP down. You'll use it constantly.
Step 1.3: SSH In from Your Second Machine
On your Mac (or second Linux machine), open a terminal and run:
ssh your-username@<LINUX_IP>
First time connecting? You'll see a fingerprint warning. Type yes. This is normal; your Mac is verifying it hasn't talked to this machine before.
Part 2: Install k3s on the Control Plane (Linux Machine)
Why k3s Instead of "Full" Kubernetes?
Standard Kubernetes (kubeadm) requires multiple system services configured separately, etcd set up manually, minimum 2GB RAM just for the control plane, plus certificate management and networking plugins.k3s is a lightweight, CNCF-certified Kubernetes distribution by Rancher (now SUSE). It bundles everything into a single binary under 100MB. Same Kubernetes API, same kubectl, same YAML manifests; just less operational overhead.
Step 2.1: Install k3s
In your SSH session to the Ubuntu machine, run:
curl -sfL https://get.k3s.io | sh -
The script detects your OS and architecture (linux/amd64 in our case), downloads the k3s binary, sets up k3s as a systemd service, and configures it as a server (control plane + worker node). It also installs a bundled containerd, flannel, CoreDNS, and Traefik.
Step 2.2: Verify the Control Plane Is Running
sudo k3s kubectl get nodes
Expected output:
NAME STATUS ROLES AGE VERSION
master-fluffy-ms-7c94 Ready control-plane 60s v1.34.5+k3s1
If you see Ready, your control plane is working.
Step 2.3: Get the Join Token
For another machine to join this cluster, it needs a secret token:
sudo cat /var/lib/rancher/k3s/server/node-token
Save this token. You'll need it in Part 3 when joining the Mac as a worker node.
Part 3: Join the Mac as a Worker Node
This is where it gets interesting. We're adding a second machine, with a different CPU architecture, to the cluster.
The Challenge
k3s doesn't run natively on macOS. It's a Linux application. So we need a Linux virtual machine on the Mac. We'll use Multipass, which creates lightweight Ubuntu VMs.
Step 3.1: Install Multipass on the Mac
brew install --cask multipass
Step 3.2: Create an Ubuntu VM with Bridged Networking
This is the step most tutorials get wrong. By default, Multipass creates VMs on an internal NAT network. Your Mac can reach the VM, but your Ubuntu PC cannot. Kubernetes needs bidirectional connectivity between all nodes.The fix: bridge the VM onto your home network so it gets an IP on the same subnet as the Ubuntu PC.
You should see two IPv4 addresses. One on 192.168.2.x (NAT, ignore this) and one on 192.168.101.x (bridged, this is the one we need).
Step 3.3: Join the Cluster
Here's the second gotcha: the VM has two network interfaces, and both k3s and its networking layer (flannel) will default to the NAT interface, the one the Ubuntu PC can't reach. You need to explicitly tell k3s to use the bridged interface.
--node-ip=<VM_BRIDGED_IP> tells kubelet to advertise the bridged IP to the control plane
--flannel-iface=enp0s2 tells flannel to send VXLAN tunnel traffic over the bridged interface
What happens if you skip these flags: the node will appear Ready in kubectl get nodes, and you'll think everything works. But pods on the worker node won't be able to reach pods on the control plane (or vice versa), because flannel's VXLAN tunnels are going through the NAT network. DNS resolution will fail, Services won't route cross-node, and you'll spend an hour debugging. Ask us how we know.
Step 3.4: Verify the Two-Node Cluster
sudo k3s kubectl get nodes -o wide
To confirm the architectures:
sudo k3s kubectl get nodes -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.status.nodeInfo.architecture}{"\n"}{end}'
k3s-worker arm64
master-fluffy-ms-7c94 amd64
Two nodes. Two architectures. One cluster.
Part 4: Deploy Your First Workload
Kubernetes Concepts You Need First
Pod: The smallest deployable unit. Usually one container. Pods are ephemeral; they're created, they run, they die.Deployment: A declaration of "I want N copies of this pod running at all times." If a pod dies, the deployment controller creates a new one.Service: A stable network endpoint for a set of pods. Pods come and go (different IPs each time), but a Service provides a fixed address.
The scheduler distributed pods across both nodes. It balanced the load automatically. The nginx image ran on both amd64 and arm64 without any special configuration because nginx is a multi-arch image.
Step 4.3: Expose It as a Service
sudo k3s kubectl expose deployment nginx --port=80 --type=NodePort
sudo k3s kubectl get service nginx
Open http://192.168.101.26:<NODE_PORT> in your browser. That request went through the Service, which routed it to one of the four pods running on either machine.
The curl pod might be on the Ubuntu PC, hitting an nginx pod on the Mac, or vice versa. Kubernetes networking made them find each other by name, across physical machines, transparently.
Part 5: Architecture-Aware Scheduling
So far, the scheduler has been placing pods wherever it wants. But what if you need a pod to run on a specific architecture?
Node Labels
Every node has labels. k3s automatically sets kubernetes.io/arch to amd64 or arm64.
This says "I'd prefer arm64, but amd64 is fine if arm64 is full."
Part 6: Clean Up and Manage
Scale a Deployment
sudo k3s kubectl scale deployment nginx --replicas=8 # scale up
sudo k3s kubectl scale deployment nginx --replicas=2 # scale down
sudo k3s kubectl get pods -w # watch in real time
Drain a Node (Graceful Maintenance)
sudo k3s kubectl drain k3s-worker --ignore-daemonsets --delete-emptydir-data
sudo k3s kubectl get pods -o wide -w # watch pods migrate
sudo k3s kubectl uncordon k3s-worker # bring it back
Uninstall k3s
On the Ubuntu PC: /usr/local/bin/k3s-uninstall.shOn the Mac VM: /usr/local/bin/k3s-agent-uninstall.shTo delete the VM: multipass delete k3s-worker && multipass purge
Part 7: Managing from Your Mac
Option A: SSH Alias (Simple)
alias k='ssh master-fluffy@192.168.101.26 "sudo k3s kubectl"'
Now k get nodes -o wide works from your Mac.
Option B: SSH Tunnel + Kubeconfig (Advanced)
sudo k3s kubectl config view --raw # on Ubuntu, copy the output
ssh -f -N -L 6443:127.0.0.1:6443 master-fluffy@192.168.101.26
KUBECONFIG=~/.kube/k3s-config.yaml kubectl get nodes -o wide
Why not just change the server IP? k3s uses separate TLS certificate authorities for server and client authentication. The SSH tunnel avoids this by presenting the connection as local, where k3s accepts both CAs.
Glossary
Term
What It Is
Cluster
A set of machines running Kubernetes
Node
A single machine in the cluster
Control plane
The node(s) running the brain
Pod
One or more containers running together
Deployment
"Keep N copies of this pod running"
Service
A stable address for a group of pods
kubectl
CLI tool to talk to Kubernetes
k3s
Lightweight Kubernetes distribution
NodeSelector
"Run this pod only on matching nodes"
Drain
Gracefully empty a node for maintenance
What's Next
Deploy a real application with multiple services talking to each other
Set up monitoring with Prometheus + Grafana
Try Helm, the package manager for Kubernetes
Experiment with persistent storage
Break things on purpose: kill pods, drain nodes, disconnect network. See how Kubernetes responds.
Built with a Ryzen 5 5600X desktop running Ubuntu 24.04 and a MacBook Pro M4 Pro, connected over a home network. Total cost beyond hardware you already own: $0.