k3sm licensed apache-2.0 (DCO)

Supported Workloads


k3sm runs OCI images two ways, and you pick per Pod:

  • darwin/arm64 images run as native Mac processes. This is the default and the fastest path, with no VM, no kernel to boot, and access to Metal and CoreML.
  • linux/arm64 images run in a Linux micro-VM. Add one line to the Pod spec, runtimeClassName: vm, and an ordinary Linux image runs unmodified. Any multi-arch image with an arm64 variant qualifies, which is most published images, on a single node.

Either way the workflow is the one you already know: build, tag, push, pull, digests, imagePullPolicy, imagePullSecrets. k3sm build builds both kinds.

amd64 runs on no path today. An amd64-only image does not start, natively or under vm, because the Linux guest would need in-guest translation, which is held for a later release.

Default Path#

The normal path is an OCI image with a Mac binary inside:

apiVersion: v1
kind: Pod
metadata:
  name: app
spec:
  # Both are required on every k3sm Pod: the nodeSelector is enforced at
  # admission (the os=darwin intent guard) and the toleration clears the
  # provider taint the node carries. Omit either and the Pod is rejected or
  # never scheduled — see [Quickstart](/docs/quickstart/).
  nodeSelector:
    kubernetes.io/os: darwin
  tolerations:
    - key: k3sm.io/provider
      operator: Exists
      effect: NoSchedule
  containers:
    - name: app
      image: myapp:v1        # a real image reference: registry, tag or digest

k3sm pulls it, verifies the digest, unpacks the layers, merges the image config (ENV, ENTRYPOINT, WORKDIR, CMD) the way Kubernetes does, and executes the entrypoint as a native Darwin process under a Seatbelt profile.

You can also run a host binary with no image at all, for a quick loop or a binary you do not want to package:

      image: native
      command: ["/opt/myapp/bin/myapp", "--flag"]

See Images for both conventions in full.

Helm charts install through HelmChart objects, the k3s shape; see Helm charts.

Linux Path#

A stock image from a public OCI registry (nginx, postgres, redis) carries a Linux userland and expects a Linux kernel. k3sm gives it one. Add runtimeClassName: vm and the image runs in its own Linux micro-VM on the same node, under the same Kubernetes semantics:

apiVersion: v1
kind: Pod
metadata:
  name: web
spec:
  runtimeClassName: vm        # <- the whole requirement
  nodeSelector:
    kubernetes.io/os: darwin
  tolerations:
    - key: k3sm.io/provider
      operator: Exists
      effect: NoSchedule
  containers:
    - name: web
      image: nginx:alpine     # linux/arm64

The nodeSelector and tolerations stanza is on every k3sm Pod, vm or not, because each node carries the k3sm.io/provider:NoSchedule taint. The vm-specific part is the one runtimeClassName line.

kubectl logs, exec -it with a terminal, attach, probes, PersistentVolumeClaims, cluster DNS and ClusterIP Services all work there; vm RuntimeClass is the full page.

Two facts are worth planning around. Each Pod gets its own VM, so it costs a boot and a VM’s memory where a native process costs neither, and the native path stays the fast one for anything you can compile for the Mac. The image must also carry an arm64 variant, since an amd64-only image does not start.

A platform mismatch, whether a Linux image on the default path or an amd64-only image on the vm path, surfaces as a Pod in ProviderFailed with a ProviderCreateFailed event naming both sides:

no image manifest matches a runnable platform: want [linux/arm64/v8], image provides [linux/amd64]

k3sm decides this before it starts anything. A Linux container runtime handed the wrong architecture starts the container and lets it die with a format error that names nothing; being told which platforms the image offers, and which one the node needs, is what you can act on.

From a Dockerfile to a Running Pod#

This is the whole loop on one node, with nothing left out. Build a darwin/arm64 binary first with go build, clang, or whatever you normally use, then:

# 1. package it
cat > Dockerfile <<'DOCKERFILE'
FROM scratch
COPY dist/myapp /usr/local/bin/myapp
ENV LOG_LEVEL=info
ENTRYPOINT ["/usr/local/bin/myapp"]
DOCKERFILE

# 2. build it — the image is in this node's store when this returns
k3sm build --tag myapp:v1 .

# 3. run it — a manifest, not `kubectl run`, which cannot set the
#    nodeSelector and toleration every k3sm Pod requires
kubectl apply -f - <<'EOF'
apiVersion: v1
kind: Pod
metadata:
  name: myapp
spec:
  nodeSelector:
    kubernetes.io/os: darwin
  tolerations:
    - key: k3sm.io/provider
      operator: Exists
      effect: NoSchedule
  restartPolicy: Never
  containers:
    - name: myapp
      image: myapp:v1
EOF

kubectl logs myapp

A Pod’s output is written to /var/log/pods/<namespace>_<pod>_<uid>/<container>/<restartCount>.log in the CRI format, rotated, and served from there. Container logs covers the layout, the kubectl logs options, --previous, and how to ship logs off a node.

To move the image off that node, pick a sink. They compose, and the store recording happens in every case:

k3sm build --tag registry.example.com/me/myapp:v1 --push .   # to a registry you name
k3sm build --tag myapp:v1 --push .                           # to this node's own registry
k3sm build --tag myapp:v1 --output myapp.tar .               # to a file, for `k3sm image load`

For a Linux workload, name the platform and run it with runtimeClassName: vm:

k3sm build --tag myapp:v1 --platform linux/arm64 .

k3sm build takes any Dockerfile. One that only copies files in is packaged on the spot, with no cluster involved; one with RUN builds on the cluster’s build engine, which starts on first use (see Building images). FROM takes scratch or a registry reference; a named base is fetched for darwin/arm64 and refused if it declares any other platform. Every deliberate difference from docker build is in Images.

Pushing a bare tag needs the node’s own registry running, and it is off unless you pass --registry-port (or use k3sm dev, which turns it on for you). It is worth the extra step when you want the actual pull path. A Pod referencing localhost:<port>/… honors imagePullPolicy: Always, notices a moved tag, and fails the way a remote registry would, none of which k3sm image load can reproduce. See Node-local registry.

What Carries Over From Your Container and Kubernetes Workflow#

Almost all of it:

Tags and digestsYes. Pin a digest and you get exactly those bytes.
Private registriesYes. imagePullSecrets on the Pod, resolved at pull.
imagePullPolicyYes. Always, IfNotPresent and Never, with upstream meanings.
Multi-arch imagesThe manifest list is read and the entry for the path’s platform selected, darwin/arm64 natively and linux/arm64 under runtimeClassName: vm. Almost no published image carries a darwin entry (see below); most carry linux/arm64, which is what the vm path selects.
docker save / docker buildx -o type=ociYes. k3sm image load and k3sm image import ingest both.
RUN in a DockerfileYes. k3sm build builds it on the cluster’s build engine, a Linux builder that starts on first use. See Building images.
FROM <linux image>Yes, for a Linux build. k3sm build --platform linux/arm64 resolves the base on the engine. On the native darwin path the base must be darwin/arm64.
linux/arm64 imagesYes. runtimeClassName: vm, one line in the Pod spec.
kubectl logsYes, with every option: --tail, --since, --timestamps, --limit-bytes, -f, --previous, and -c for a named container. Output is written and rotated the way a kubelet writes and rotates it. See Container logs.
amd64 imagesNot yet, on either path. An amd64-only image does not start; a multi-arch image with an arm64 variant runs under vm by selecting that variant.

The apiserver defaults a :latest tag to imagePullPolicy: Always, so a :latest image you loaded locally will still be fetched from a registry. This is the one people hit. Tag with something else, or set the policy explicitly.

Where darwin Images Come From#

There is no supply of them. You build them.

“k3sm runs OCI images” can otherwise be read as “k3sm runs the images you can already docker pull”. Checked against the public registry those images are published on:

imageplatforms publisheddarwin?
alpine, nginx, postgres, redis8 each, every one linux/*none
golang9, linux and windowsnone

golang is the informative row. The OCI os field really does carry non-Linux values in the wild, and Windows containers are published. Darwin ones are published nowhere, because macOS has no container primitive to build them on, so the ecosystem never formed.

The format, the registries and the tooling are the ones you know, and darwin content is yours to produce. k3sm build packages a Mac binary into an ordinary OCI image and --push puts it in your registry; nodes pull it the ordinary way. An existing public image is a Linux image, and that is what runtimeClassName: vm is for.

Summary#

The default path runs Mac-native workloads with Kubernetes semantics and the OCI toolchain around them, including the ones that need Metal or CoreML, which no Linux VM can give you. Unmodified linux/arm64 images run on the same cluster, one runtimeClassName: vm line away, at the cost of a VM per Pod, and most published images carry an arm64 variant. amd64 payloads run on no path today; if that is your workload, Limitations has the full picture.