Your First Kubernetes Cluster the Hard Way with OrbStack

This is the starting point. If you’ve never built a Kubernetes cluster from scratch — no kubeadm, no managed services, just raw binaries and real certificates — this is where to begin. OrbStack is the recommended first tool because it uses the fewest resources, machines appear in seconds, and your Mac stays cool even with 6 VMs running.

By the end of this post, you’ll have a working Kubernetes 1.32.0 cluster with a dedicated Vault server for PKI, a bastion/jump server, a separate etcd node, one control plane master, and two worker nodes — all running on your MacBook. The entire deployment takes about 6 minutes.

This post covers the OrbStack Simple setup. Want to understand the architecture and design decisions before diving in? Read the OrbStack Simple overview first. For help choosing between OrbStack, UTM, and Vagrant, see the UTM vs Vagrant vs OrbStack comparison. For the full learning path from Simple to HA, see From Simple to HA: A Learning Path for Kubernetes on Apple Silicon.

What You’re Building

The “simple” setup isn’t a toy. It includes a HashiCorp Vault server managing a 3-tier PKI CA hierarchy, a jump/bastion server that serves as the single SSH entry point and Ansible controller, a dedicated etcd node, and Kubernetes installed the hard way from individual ARM64 binaries. The only thing “simple” about it is a single control plane node instead of the HA setup’s two masters behind HAProxy.

The 6 machines and their roles:

MachineRoleStatic IP
vaultPKI & certificate management192.168.139.11
jumpBastion / Ansible controller / kubectl192.168.139.12
etcd-1Key-value store for cluster state192.168.139.21
master-1Control plane (apiserver, controller-manager, scheduler)192.168.139.31
worker-1Worker node (kubelet, kube-proxy, containerd)192.168.139.41
worker-2Worker node (kubelet, kube-proxy, containerd)192.168.139.42

All machines run Ubuntu 24.04 (Noble) ARM64. The network prefix (192.168.139.x) is auto-detected from OrbStack’s configuration at deploy time.

Why OrbStack for Your First Cluster

OrbStack’s Linux machines share the host kernel rather than booting their own — they’re lightweight environments, not full VMs. The practical impact: each machine uses only 1.3–3.0 GB of disk (the full 6-machine setup is roughly 10 GB total), memory isn’t pre-allocated per machine, and machines are created with orb create in seconds rather than minutes.

For learning Kubernetes — deployments, services, RBAC, pod networking, certificate management — this is indistinguishable from a full VM. The 5% of cases where shared-kernel differences matter (custom kernel modules, kernel-level security policies) aren’t relevant when you’re learning the fundamentals.

If you want full VM isolation instead, the same cluster architecture is available with UTM and Vagrant. The Ansible automation is identical across all three — only the machine creation layer changes.

Prerequisites

You need a Mac with Apple Silicon (M1/M2/M3/M4) and OrbStack installed (free for personal use). That’s it. No Homebrew packages, no plugins, no provider configuration. OrbStack handles everything.

Generate an SSH key if you don’t already have one at ~/.ssh/k8slab.key:

ssh-keygen -t ed25519 -f ~/.ssh/k8slab.key -C "k8s-homelab" -N ""

Deploy in One Command

git clone https://github.com/labitlearnit/k8s-orbstack-simple-homelab.git
cd k8s-orbstack-simple-homelab
bash scripts/k8s-orbstack-simple-homelab.sh

The script handles everything: creating 6 OrbStack machines with cloud-init, downloading Kubernetes binaries in the background, configuring the jump server as the bastion and Ansible controller, bootstrapping Vault with a 3-tier PKI hierarchy, issuing TLS certificates for every component, deploying etcd, the control plane, worker nodes, and Calico CNI.

Total deployment time: approximately 5 minutes 59 seconds from cold start to kubectl get nodes showing all nodes Ready.

What the Script Does — The Short Version

The deploy script runs in three phases:

Phase 1 — Machine creation. The script calls orb create ubuntu noble {name} for each of the 6 machines, passing cloud-init YAML configs that set hostnames, create the k8s user with passwordless sudo, inject the SSH public key, write /etc/hosts with all machine entries, and configure static IPs via Netplan. Machines appear in seconds.

Phase 2 — Jump server setup. The SSH private key is copied to jump. The project’s Ansible directory is synced via SCP. An SSH config is written on jump with entries for all 5 other machines. Kubernetes binaries (downloaded on the Mac in the background during Phase 1) are cached on jump so Ansible roles don’t need to download them inside each machine.

Phase 3 — Ansible deployment. From the jump server, the script executes Ansible playbooks in sequence: Vault bootstrap and PKI setup (3-tier CA with separate CAs for Kubernetes, etcd, and front proxy), certificate issuance for all components, etcd deployment with TLS, control plane deployment (kube-apiserver, controller-manager, scheduler as systemd services), worker node deployment (containerd, kubelet, kube-proxy), and Calico CNI installation.

Accessing Your Cluster

After deployment, SSH to the jump server and you’re in:

ssh jump
kubectl get nodes -o wide

You should see two worker nodes in Ready state. Masters don’t appear in the node list because this is a hard-way setup — control plane components run as systemd services directly on the master node, not as pods managed by kubelet. Only workers run kubelet and register with the API server.

From jump, you can reach any other machine:

ssh vault # Inspect Vault PKI
ssh etcd-1 # Check etcd cluster
ssh master-1 # View control plane services
ssh worker-1 # Check kubelet and containerd

You can also open the Vault UI at http://vault:8200 in your Mac’s browser (the deploy script adds a host entry for vault to your Mac’s /etc/hosts).

What to Explore First

On the master node — check the control plane services: systemctl status kube-apiserver, systemctl status kube-controller-manager, systemctl status kube-scheduler. Read the unit files to see every flag that was passed to each component.

On a worker node — check the container runtime and node agent: systemctl status containerd, systemctl status kubelet, systemctl status kube-proxy. Look at the kubelet config to see how it connects to the API server.

On the etcd node — verify the data store: systemctl status etcd. The etcd server runs with TLS — check the certificate paths in the systemd unit file and verify they’re signed by the etcd CA, not the Kubernetes CA.

On your Mac — open the UI at http://vault:8200 and explore the 5 PKI mounts: pki_root, pki_int, pki_kubernetes, pki_etcd, pki_front_proxy. This is the 3-tier CA hierarchy described in the Vault PKI deep dive.

Break Things — That’s the Point

The real learning starts when you break the cluster intentionally:

Stop the API server: ssh master-1, then sudo systemctl stop kube-apiserver. Run kubectl get nodes from jump — it fails. But check the workers: pods keep running. The API server is the control plane, not the data plane. Start it back up (sudo systemctl start kube-apiserver) and everything recovers.

Stop kubelet on a worker: ssh worker-1, then sudo systemctl stop kubelet. Wait 40 seconds (the default node-monitor-grace-period). The node transitions to NotReady. Pods on that worker get rescheduled to worker-2. Restart kubelet and the node recovers.

Stop etcd: ssh etcd-1, then sudo systemctl stop etcd. The API server can’t read or write any state. kubectl commands hang or return errors. This is why the HA setup uses 3 etcd nodes — so one can fail without affecting the cluster.

These experiments build the operational intuition that no tutorial can teach. You’re seeing exactly what happens when each component fails, which is exactly what production oncall looks like.

OrbStack-Specific Things to Know

Two IPs on one interface. Each OrbStack machine has two IP addresses on eth0 — the static IP from cloud-init (192.168.139.x) and a second IP assigned by OrbStack internally. This doesn’t cause issues because all components are configured to bind to the specific static IP, but it can surprise you during debugging. Always verify with ip addr show eth0.

All 6 OrbStack machines each showing two IPs on eth0: static 192.168.139.x for K8s binding and OrbStack-internal 192.168.139.x
Every OrbStack machine carries two IPs on eth0 — the static 192.168.139.x address (yellow) that all K8s components bind to, and the OrbStack-internal 192.168.139.x address (red) that OrbStack assigns internally. Always check with ip addr show eth0 when debugging.

Swap handling. OrbStack uses zram swap that can’t be disabled at the kernel level. The kubelet configuration sets failSwapOn: false, which is the intended approach for Kubernetes 1.32.0 where the NodeSwap feature gate is beta and enabled by default.

VS Code terminal. VS Code’s integrated terminal may not route traffic to OrbStack’s static IPs correctly. If SSH commands fail from VS Code but work from macOS Terminal, switch to Terminal for cluster management.

Component Versions

Every component version is locked for reproducibility. These are identical across the UTM and Vagrant simple setups — the only difference is the virtualization layer:

Kubernetes1.32.0
etcd3.5.12
containerd1.7.24
runc1.2.4
Calico CNI3.28.0
Vault1.15.4

Tearing Down

When you’re done experimenting:

bash scripts/destroy-vms.sh

All 6 machines are removed. The entire cycle — deploy, explore, break, destroy, redeploy — takes about 12 minutes. This fast iteration loop is one of OrbStack’s biggest advantages.

What’s Next

Once you’re comfortable with the simple cluster — you’ve explored each component, broken and recovered services, read the Ansible playbooks — you’re ready for the next level. The Simple to HA learning path maps the full progression.

The HA setup adds 5 more machines: HAProxy for API server load balancing, a second master for control plane redundancy, two more etcd nodes for a 3-node consensus cluster, and a third worker. See the OrbStack HA deep dive for the full walkthrough.

To understand why the simple cluster isn’t production-ready and what the HA setup fixes, read Why Your Homelab K8s Cluster Isn’t Production-Ready.

For a deep dive into the Vault PKI hierarchy powering the certificates in this cluster, see Vault PKI for Kubernetes: 3-Tier CA the Right Way.

The full source code is at github.com/labitlearnit/k8s-orbstack-simple-homelab.

Big tech, small lab. One reel at a time.

Questions, corrections, or want to share how you’re using these repos?

labitlearnit@gmail.com

Enjoyed this post?

Want homelab configs to your email?

Leave a Reply

Discover more from Lab it, learn it

Subscribe now to keep reading and get access to the full archive.

Continue reading