Supported Workloads
k3sm runs OCI images two ways, and you pick per Pod:
darwin/arm64images 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/arm64images 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 anarm64variant 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 digests | Yes. Pin a digest and you get exactly those bytes. |
| Private registries | Yes. imagePullSecrets on the Pod, resolved at pull. |
imagePullPolicy | Yes. Always, IfNotPresent and Never, with upstream meanings. |
| Multi-arch images | The 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=oci | Yes. k3sm image load and k3sm image import ingest both. |
RUN in a Dockerfile | Yes. 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 images | Yes. runtimeClassName: vm, one line in the Pod spec. |
kubectl logs | Yes, 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 images | Not 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:
| image | platforms published | darwin? |
|---|---|---|
alpine, nginx, postgres, redis | 8 each, every one linux/* | none |
golang | 9, linux and windows | none |
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.