Orchard — Kubernetes clusters on Apple Silicon with apple/container
I’ve been wanting a lightweight way to run Kubernetes directly on my MacBook’s Apple Silicon without firing up a whole VM farm. minikube doesn’t have an apple/container driver, and Apple’s own container CLI has an unmerged k8s proposal that never shipped. Orchard fills that gap — it drives kubeadm against node VMs booted with apple/container, no third-party shim in the loop.
What Orchard is
A CLI tool (Go) that creates multi-node Kubernetes clusters where every node is its own lightweight Linux VM running on Apple’s native container CLI (Virtualization.framework). Each node boots from a kindest/node image (same image kind uses) with systemd, containerd, kubeadm, and kubelet pre-installed.
No Virtual Kubelet, no Docker Desktop, no Lima — just kubeadm init and kubeadm join running inside native Apple VMs.
Prerequisites
- macOS on Apple Silicon (M-series)
apple/containerinstalledkubectl
container system start
container system kernel set --recommended
Install
brew install rahulkj/tap/orchard
orchard doctor # verify everything is ready
Or build from source: make build.
Quick start
orchard create --name dev --workers 2
Wait ~30 seconds, then:
kubectl get nodes -o wide
container list # same nodes, as VMs
You get:
- 1 control-plane VM + 2 worker VMs
kindnetCNI (default, cross-node pod connectivity verified)- metrics-server + local-path StorageClass
orchard-devcontext merged into~/.kube/config
Anatomy of a cluster
| Component | Detail |
|---|---|
| Node image | kindest/node (pinned by digest, currently v1.36.1) |
| VM hypervisor | Apple container (Virtualization.framework) |
| Kubernetes init | kubeadm init via container exec |
| CNI | kindnet (default), flannel (broken — missing bridge plugin), calico (untested) |
| Default sizing | 4 CPU / 4 GB control-plane, 4 CPU / 2 GB per worker |
| State | Saved locally; kubeconfig context orchard-<name> |
Commands
orchard create --name dev --workers 2 # create
orchard scale --name dev --workers 4 # scale up/down
orchard stop --name dev # stop VMs (preserve state)
orchard start --name dev # resume VMs
orchard delete --name dev # teardown
orchard list # list clusters
orchard doctor # health check
orchard check-updates # check newer k8s versions
orchard upgrade --name dev # recreate on new image
orchard headlamp install --name dev # web UI
orchard headlamp token --name dev # access token
orchard self-update # update orchard binary
Create flags
| Flag | Default | Notes |
|---|---|---|
--workers |
2 |
0 gives single untainted node |
--image |
pinned digest | override for different k8s version |
--cp-cpus / --cp-memory |
4 / 4096M |
control-plane sizing |
--worker-cpus / --worker-memory |
4 / 2048M |
per-worker sizing |
--cni |
kindnet |
kindnet, flannel, or calico |
--no-metrics |
off | skip metrics-server |
--no-storage |
off | skip local-path StorageClass |
--headlamp |
off | install Headlamp web UI |
--proxy-forward |
off | corporate proxy / Zscaler support |
The tricky parts Orchard handles
Fresh DHCP lease on every start
Apple container VMs get a new IP every boot. kindest/node images aren’t designed for that — kind create writes a /kind/kubeadm.conf that the image’s entrypoint expects on IP change, and it never repoints kube-proxy or CoreDNS at a new control-plane address. Left stale, every ClusterIP silently breaks after a restart.
orchard start repairs all of this:
- Regenerates the API server serving cert for the new IP
- Repoints
admin.conf,super-admin.conf, and every worker’skubelet.conf - Repoints the cluster-wide
kube-proxyConfigMap and restarts kube-proxy - Restarts CoreDNS (its in-process client latches onto the first connection and won’t recover)
No internet egress from VMs
apple/container pulls images host-side. Every addon URL (metrics-server, Headlamp, CNI manifests) is fetched on the host and piped into the guest:
container exec -i <node> kubectl apply -f -
kubeadm init version pinning
Kubeadm’s default “resolve latest stable from the internet” can return a version newer than what’s baked into the image, causing image pull failures. Orchard reads /kind/version from inside the image and pins --kubernetes-version to it.
Scaling
orchard scale --name dev --workers 4
Scaling up boots and joins new workers at the lowest free indices. Scaling down drains the highest-indexed workers first (kubectl drain → kubectl delete node → VM removal), keeping lower-numbered workers stable across resizes.
Corporate proxy support
Macs behind Zscaler/Netskope-style TLS-intercepting proxies need the interception root CA trusted inside the guest VMs. --proxy-forward does two things:
- Exports certs from
System.keychain(where MDM/security-agent roots land) and installs them inside the guest viaupdate-ca-certificates - Forwards
HTTP_PROXY/HTTPS_PROXYenv vars into the guest, rewriting127.0.0.1/localhostto the guest’s default gateway
It deliberately skips scutil --proxy/PAC — on managed Macs, PAC scripts point at a loopback port bound by the local security agent that a guest VM can’t reach.
Headlamp web UI
orchard create --name dev --headlamp
# or install into existing cluster:
orchard headlamp install --name dev
orchard headlamp token --name dev
kubectl port-forward -n kube-system service/headlamp 8080:80
Headlamp is a solid Kubernetes web UI. The upstream manifest references a headlamp-admin ServiceAccount that doesn’t exist — Orchard creates it, bound to cluster-admin (appropriate for a local dev cluster).
Upgrade
orchard upgrade --name dev
This is a destroy-and-recreate, not an in-place kubeadm upgrade. The node images are immutable single-version builds with no internet egress, so there’s no way to do an in-place version bump — kind has the same constraint. Orchard reads the cluster’s saved settings, deletes it, and recreates on the new image. Workloads are not preserved.
Why not alternatives?
| Tool | Gap |
|---|---|
minikube |
No apple/container driver (open issue, unimplemented) |
apple/container k8s plugin |
Experimental proposal, never merged/shipped |
kiac / macvz / apple-container-kubelet |
Virtual Kubelet approach, not real multi-node kubeadm |
kind / k3d |
Runs containers inside Docker, not native VMs |
Orchard gives you real kubeadm-driven multi-node clusters running in native Apple VMs — no Docker dependency, no indirection layer, just container run → kubeadm init.
If you have an Apple Silicon Mac and want to poke at a real multi-node Kubernetes cluster without spinning up a homelab or paying for cloud VMs, give Orchard a shot.
brew install rahulkj/tap/orchard
orchard create --name dev --workers 2
kubectl get nodes
Enjoy!