k3sm licensed apache-2.0 (DCO)

Building an image

Package a native darwin/arm64 binary into an OCI image: recorded in the node’s image store, and exportable as a portable artifact for registries and any other OCI tool.


In about 10 minutes you produce a reproducible OCI image containing your native binary, recorded in this node’s image store under its tag. An image: myapp:v1 Pod spec resolves the moment the build finishes. Add --output and the build additionally writes a portable artifact to a path you name, for a registry, another node (the next tutorial), or any other OCI-compatible tool.

1. The Dockerfile#

The native packaging path executes nothing. It copies files and writes metadata, which is why its accepted grammar is small. (A Dockerfile with RUN steps builds on the cluster’s build engine instead; see section 4.)

FROM scratch
COPY dist/myapp /usr/local/bin/myapp
ENV LOG_LEVEL=info
EXPOSE 8080
ENTRYPOINT ["/usr/local/bin/myapp"]

The accepted subset is FROM (scratch or a registry reference), COPY, ADD, ENV, ENTRYPOINT, CMD, WORKDIR, LABEL, and EXPOSE. Anything else is refused with an error naming what was rejected; the parser never silently drops an instruction, because a dropped instruction produces an image that does not match its recipe.

2. Build It#

GOOS=darwin GOARCH=arm64 go build -o dist/myapp ./cmd/myapp
k3sm build --tag myapp:v1 --output myapp.tar .

The tarball is a standard docker-save archive, so other tools accept it directly:

docker load -i myapp.tar

For an OCI layout directory instead:

k3sm build --tag myapp:v1 --output ./layout --format oci .

3. The Differences That Will Bite You#

Each of these is a refusal or a documented gap, never a silent divergence.

k3sm build
RUNnot part of the native packaging path; routes to the cluster’s build engine (see Building images)
FROM <ref>supported, with two caveats. The base is fetched for darwin/arm64 and refused if it declares any other platform. A tag-pinned base is not reproducible, so the build says so and prints the base it used; pin a digest for the guarantee
ADDan exact alias of COPY, with no remote URL fetch and no archive auto-extraction; both are refused rather than downgraded
.dockerignorenot implemented, so COPY . includes .git, .env, and everything else. Scope your COPY lines, or build from a clean tree
--platformaccepts darwin/arm64, packaged natively, and Linux targets, which build on the cluster’s engine; neither builder cross-compiles
$VAR, ARGno expansion is performed. ARG is rejected, and so is a $ in a COPY, ADD, or WORKDIR path, where it would be an invisible divergence. A $ in an ENV or LABEL value passes through as a literal
outputrecorded in this node’s image store under its tag; --output additionally writes a portable artifact to the path you name; nothing is pushed unless you add --push
timestampsfixed rather than wall-clock, so rebuilding the same context yields the same digest
file modesnormalized to 0755 or 0644; a 0600 source becomes world-readable, and setuid, setgid, and sticky bits are dropped
ownership and xattrsevery entry is uid 0 / gid 0 with no xattrs, so your account identity and macOS quarantine flags never reach the image
special filesdevices, FIFOs, and sockets in the context are refused rather than skipped; a symlink whose target escapes the image root is refused
WORKDIRsets the config’s working directory but does not create it. On FROM scratch there is no base filesystem, so add a COPY if the directory must exist

The two that surprise people most are .dockerignore and file modes. Scope your COPY lines narrowly, and do not rely on a source file’s permissions surviving into the image.

4. RUN Steps#

A Dockerfile with a RUN line does not fail here. It routes to the cluster’s build engine, a managed BuildKit builder running inside a vm-RuntimeClass micro-VM, which k3sm build starts on first use. The engine runs only on a Mac that can boot a VM guest; elsewhere its Pod stays Pending and a build with RUN steps cannot proceed. The image lands in the node’s store under its tag either way; see Building images for the engine’s own lifecycle (k3sm builder status/up/down) and the k3sm builder buildx passthrough. Registry pull works the same regardless of how an image was built. An image: ghcr.io/org/app:tag Pod is pulled, digest-verified, and run with kubelet-faithful semantics, provided the payload is darwin/arm64 (a Linux image runs under the vm RuntimeClass instead).

Next#

  • loading an image puts the artifact into a node’s store.
  • images is the reference page, with the full comparison tables.