k3sm licensed apache-2.0 (DCO)

Your first native pod

Install k3sm, start a node, run a Pod, and find that Pod in ps as an ordinary macOS process.


About 15 minutes, most of it the install. By the end you have a single-node k3sm cluster on your Mac, one running Pod, and that same Pod visible in ps as a native Darwin process owned by _k3sm.

On every other Kubernetes-on-a-Mac, the thing you started is a process inside a Linux VM, and your Mac cannot see it. On k3sm it is a process on your Mac.

1. Install#

curl -fsSL https://k3sm.io/install.sh | sh

The script preflights for Apple Silicon and macOS 26+, downloads the release tarball and its checksums, verifies the sha256, prints what it is about to do, and then runs sudo k3sm install for you. That one admin step creates the unprivileged _k3sm user, installs the root networking helper and the launchd daemons, and writes an admin kubeconfig into your home directory. Everything after it runs with no sudo.

The script installs the newest published release, a pre-release build that is ad-hoc signed and not notarized. Set K3SM_INSTALL_DOWNLOAD_ONLY=1 to download and verify without running anything privileged. install covers how to pin a version and the three install channels, of which only the script is published so far.

2. Control Plane and Node#

The install registered the server as a launchd daemon, so the control plane and the node are already running and come back after a reboot. Do not start a second copy. The command below is for a throwaway foreground cluster on a Mac with no install, and running it beside the daemon makes the two fight over the work directory and the apiserver port:

k3sm server

Either way, one process supervises the upstream kube-apiserver, controller-manager, and scheduler over kine and SQLite, and runs the Virtual Kubelet node beside them. To check the installed one, run k3sm status: one screen covering the daemons, the apiserver, the node, workloads and the data root, with an exit code a script can branch on.

3. Talk to the Cluster#

k3sm kubectl get nodes

k3sm kubectl is the bundled passthrough, using the admin context the install merged into your kubeconfig, so your own client reaches the cluster too. If you write the admin kubeconfig to a file of its own, point KUBECONFIG at that file; kubectl access shows how. The rest of these pages write k3sm kubectl. A standalone kubectl behaves identically, because the API server is the genuine upstream one.

4. Run a Pod#

On this default path a k3sm workload is a native arm64 Darwin executable. image: native is a sentinel meaning “the workload is command, whose first element is an absolute path to a host binary”.

apiVersion: v1
kind: Pod
metadata:
  name: hello-native
  namespace: default
spec:
  nodeSelector:
    kubernetes.io/os: darwin
  tolerations:
    - key: k3sm.io/provider
      operator: Exists
      effect: NoSchedule
  restartPolicy: Never
  containers:
    - name: hello
      image: native
      command:
        - /bin/sh
        - -c
        - |
          echo 'hello from a k3sm native pod'
          /usr/bin/sw_vers
          exec /usr/bin/tail -f /dev/null

Save it as hello-native.yaml (it is the same manifest as examples/hello-native.yaml in the k3sm repository) and apply it:

k3sm kubectl apply -f hello-native.yaml
k3sm kubectl get pods -o wide
k3sm kubectl logs hello-native

The logs carry the greeting and the output of sw_vers, which is your Mac’s own macOS version.

5. See It in ps#

Find the Pod’s process:

pgrep -fl tail

That prints the pid and command line of the tail -f /dev/null the Pod is running. It is a plain process in your Mac’s process table. Ask who owns it:

ps -o user=,pid=,command= -p "$(pgrep -f 'tail -f /dev/null' | head -1)"

The owner is _k3sm, the unprivileged service user the install step created. Every Pod on the node runs as that same user, so there is no per-Pod uid isolation and same-node Pods share one OS trust domain. What that costs you, and the way out of it, are covered in what will not work and the vm RuntimeClass.

6. Clean Up#

k3sm kubectl delete pod hello-native

On the default runtime, deletion follows the upstream sequence. The preStop hook runs, the process gets SIGTERM, and after the grace period (terminationGracePeriodSeconds, 30 seconds by default) anything still alive gets SIGKILL. Check with pgrep -fl tail that it is gone.

What to Know Before You Build on This#

  • Two fields are mandatory. The kubernetes.io/os: darwin node selector is enforced by admission, and the k3sm.io/provider toleration is what lets the scheduler place the Pod. Drop either and the Pod fails to apply or never runs.
  • restartPolicy is honored on the default runtime. An exited container is restarted in place per its policy, and Always restarts even a clean exit 0. The restart count and a CrashLoopBackOff backoff read as they do upstream. The --runtime hostprocess opt-out keeps the old reap-only behavior, where a container exits once and is never respawned until a controller replaces the Pod. See limitations.
  • The workload is a binary you built. For your own app, build a darwin/arm64 binary with your usual toolchain and point command[0] at its absolute path, or set image: /abs/path and omit command entirely.

Next#