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 | |
|---|---|
RUN | not 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 |
ADD | an exact alias of COPY, with no remote URL fetch and no archive auto-extraction; both are refused rather than downgraded |
.dockerignore | not implemented, so COPY . includes .git, .env, and everything else. Scope your COPY lines, or build from a clean tree |
--platform | accepts darwin/arm64, packaged natively, and Linux targets, which build on the cluster’s engine; neither builder cross-compiles |
$VAR, ARG | no 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 |
| output | recorded 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 |
| timestamps | fixed rather than wall-clock, so rebuilding the same context yields the same digest |
| file modes | normalized to 0755 or 0644; a 0600 source becomes world-readable, and setuid, setgid, and sticky bits are dropped |
| ownership and xattrs | every entry is uid 0 / gid 0 with no xattrs, so your account identity and macOS quarantine flags never reach the image |
| special files | devices, FIFOs, and sockets in the context are refused rather than skipped; a symlink whose target escapes the image root is refused |
WORKDIR | sets 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.