RKE2/K3s on macOS + Apple Containers

# kubernetes# macos# rke2# k3s
RKE2/K3s on macOS + Apple Containersjdm

This project covers both RKE2 and K3s on macOS with Apple's container project as the runtime for...

This project covers both RKE2 and K3s on macOS with Apple's container project as the runtime for each of the processes.

rke2-silicon runs a single Kubernetes node on Apple silicon. rke2-silicon up starts the control plane as Apple containers and returns. A per-user LaunchAgent keeps the node Ready and starts each pod as its own container. Every container is a Linux micro-VM. The catalog is Kubernetes 1.36.4: RKE2 v1.36.4+rke2r1 and K3s v1.36.4+k3s1.

The node is for work that reads something on this Mac, processes it here, and writes to a store that already has an address. A camera gateway, an NFS export, a directory a bench just filled.

What is actually running

There is no rke2 binary, no containerd, and no kubelet. up writes certificates on the Mac, mounts ~/.rke2-silicon at /var/lib/rke2-silicon, and starts Rancher's hardened images with the Apple container CLI. For an RKE2 release those processes are etcd, kube-apiserver, kube-controller-manager, and kube-scheduler. config.yaml uses RKE2's keys. Those keys become process arguments. The processes do not read the file.

The LaunchAgent is the kubelet. It heartbeats the node and starts pods. CNI, kube-proxy, and CoreDNS are not installed. Traefik is a Deployment this installer applies.

The control plane and the workloads sit on 192.168.128.0/24. The Mac is 192.168.128.1 on that network. kubectl uses https://127.0.0.1:6443. Pods reach each other on that network, reach a host directory through virtiofs, and reach the GPU broker at 192.168.128.1:10443. The broker is a host process. apple.com/gpu is a concurrency limit of four, not a slice of the GPU.

LAN clients use a different address. ingress-address is a /32 alias on the LAN interface. A root LaunchDaemon accepts 80 and 443 there and connects to Traefik on 8080 and 8443, because the container CLI will not bind host ports below 1024. ClusterIP, NodePort, and LoadBalancer stay as records. Nothing routes them.

up never downloads or runs the RKE2 installer. kubernetes-version in config.yaml selects a row in the catalog, and that row names the container images Rancher already publishes for that release.

For v1.36.4+rke2r1 the row is:

  • docker.io/rancher/hardened-kubernetes:v1.36.4-rke2r1-build20260821
  • docker.io/rancher/hardened-etcd:v3.6.14-k3s1-build20260819

The up argument writes certificates and the kubeconfig on the Mac under ~/.rke2-silicon, then tasks the Apple container CLI to run four containers. The CLI pulls an image the first time that name is started. Each container mounts ~/.rke2-silicon at /var/lib/rke2-silicon and runs one binary from the image: etcd, kube-apiserver, kube-controller-manager, and kube-scheduler. The keys in config.yaml are turned into flags on those commands. The processes do not read the file.

Once the apiserver answers, up creates the node object and the ConfigMap kube-system/rke2-silicon-version through the API. The LaunchAgent keeps that node Ready and starts each pod as another container. Traefik is a Deployment this program applies. The RKE2 charts, CNI, kube-proxy, and CoreDNS are not installed.

A version that is not in the catalog fails. A later up with the same version leaves the running containers in place. A new RKE2 release is added by putting its version and image tags in the catalog, then running upgrade.

Repo & Examples

The rke2-silicon repository contains the examples and sample configurations for rke2 and k3s installations.

Requirements & Installation

You need macOS 26 on Apple silicon, the Apple container CLI 1.4.1 or newer, kubectl, Go 1.26, and passwordless sudo for /sbin/ifconfig and for installing the forwarder.

Copy examples/config.yaml and set three addresses. node-ip is this Mac. ingress-address is a free address on the same LAN, kept out of DHCP. tls-san repeats node-ip.

kubernetes-version: v1.36.4+rke2r1
node-name: dreamland
node-ip: 192.168.1.20
ingress-address: 192.168.0.200
tls-san:
  - 192.168.1.20
Enter fullscreen mode Exit fullscreen mode
go build -o rke2-silicon ./cmd/rke2-silicon
./rke2-silicon up --config config.yaml
Enter fullscreen mode Exit fullscreen mode

up creates the rke2-silicon network on first start, installs the alias, and loads the LaunchAgent. The binary the agent runs has to be the one you just built. go run throws that binary away when the process exits.

Validate Cluster Status

export KUBECONFIG=$HOME/.rke2-silicon/rke2-silicon.yaml
kubectl get --raw=/readyz
kubectl get nodes
container list --all
Enter fullscreen mode Exit fullscreen mode

/readyz returns ok. The node is Ready. container list shows rke2-silicon-etcd, rke2-silicon-kube-apiserver, rke2-silicon-kube-controller-manager, and rke2-silicon-kube-scheduler, each in state running.

Sample workload: a directory on the Mac

The instrument-drop example is a PersistentVolume whose path is a directory that already exists. The pod appends a line there through virtiofs. Create the directory first, mode 0777, so the guest can write it.

sudo mkdir -p /Users/Shared/rke2-silicon/bench
sudo chmod 0777 /Users/Shared/rke2-silicon/bench
kubectl apply -f examples/pv/workload.yaml
kubectl rollout status deployment/bench
cat /Users/Shared/rke2-silicon/bench/drop.txt
Enter fullscreen mode Exit fullscreen mode

kubectl get pvc bench shows Bound. The file contains a line like parsed 2026-10-08T19:30:00Z. That line was written by the pod and read on the Mac. The volume's reclaim policy is Retain, so deleting the claim leaves the directory in place.

kubectl delete -f examples/pv/workload.yaml
Enter fullscreen mode Exit fullscreen mode

Stop or Uninstall Cluster

stop and down stop the rke2-silicon containers and leave them on disk, along with ~/.rke2-silicon and the ingress alias. Start them again with up.

uninstall deletes those containers, including ones that are already stopped. The data directory and the ingress alias stay, so a later up can reuse the certificates and etcd data.

uninstall --purge removes the containers, the rke2-silicon network, ~/.rke2-silicon, the ingress alias, and the launchd jobs. stop --purge and down --purge do the same thing.

./rke2-silicon stop
./rke2-silicon uninstall
./rke2-silicon uninstall --purge
Enter fullscreen mode Exit fullscreen mode