Building and Loading Images Without a Registry
CKAD lists defining, building and modifying container images, and the modifying half is what actually comes up: overriding an image's entrypoint, arguments and environment from the Pod spec. This builds a real image with buildah, side-steps the registry entirely by importing it straight into containerd, runs it, and then changes its behaviour without rebuilding anything.
Application Design and Build Guide 12 of 44 Intermediate
- Kubernetes1.36.4
- Runtimecontainerd 2.2.6
- CNICalico v3.32.1
- TimeAbout 18 min
- Reviewed23 August 2026
Written against the versions above. `ENTRYPOINT` maps to the Pod's `command`, and `CMD` maps to `args`. That mapping is the thing to memorise: setting `command` in a manifest replaces the image's ENTRYPOINT, and setting `args` replaces its CMD. Setting `command` when you meant `args` silently discards the image's entrypoint and is a common way to make a working image do nothing.
| Server Name | IP Address | OS | Roles | CPU | RAM | HDD |
|---|---|---|---|---|---|---|
| CKA4001 | 192.168.0.191 | Ubuntu 26.04 LTS | Single Node (control plane, untainted) | 2 Core | 4 GB | 50 GB |
Before you start
- A node you can install packages on and run
sudo ctragainst. On a managed cluster you would use a registry instead - the build and the override steps are identical. - The session installs buildah, builds
localhost/ckad-demo:1, imports it into containerd'sk8s.ionamespace, and runs two Pods from it.
-
A builder, and a Containerfile with something to override
buildah builds OCI images without a daemon, which makes it the easiest thing to put on a Kubernetes node - there is no Docker here, and there does not need to be.
The Containerfile is deliberately small and deliberately has three things a manifest can override:
FROM busybox:1.36 ENV GREETING="built on the node" COPY hello.sh /hello.sh ENTRYPOINT ["/bin/sh", "/hello.sh"] CMD ["default-argument"]An
ENV, anENTRYPOINTand aCMD. The script prints the environment variable and its first argument, so whatever wins is visible in the logs.bash Example session sudo apt-get install -y buildah 2>&1 | tail -4 No user sessions are running outdated binaries. No VM guests are running outdated hypervisor (qemu) binaries on this host.buildah --versionbuildah version 1.42.1 (image-spec 1.1.1, runtime-spec 1.2.1)mkdir -p /tmp/ckad-build && cat > /tmp/ckad-build/Containerfile <<'EOF'FROM busybox:1.36ENV GREETING="built on the node"COPY hello.sh /hello.shENTRYPOINT ["/bin/sh", "/hello.sh"]CMD ["default-argument"]EOFprintf '#!/bin/sh\necho "$GREETING / arg=$1"\n' > /tmp/ckad-build/hello.shcd /tmp/ckad-build && sudo buildah bud -t localhost/ckad-demo:1 . 2>&1 | tail -6Copying blob sha256:c20b9e8228e83d7faccb9d270a007e03218c581932827527f41a8f7f15bd232fCopying config sha256:271554bf48cdd66b5c12a8f3bd885fc5ccb27f4ea2d6a7c723f448922fd1a307Writing manifest to image destination--> 271554bf48cdSuccessfully tagged localhost/ckad-demo:1271554bf48cdd66b5c12a8f3bd885fc5ccb27f4ea2d6a7c723f448922fd1a307Expected resultbuildah installed and an image built from five instructions.
Success conditionYou have a real image with an entrypoint, a default argument and a default environment.
-
Into the runtime, with no registry
The usual route is push to a registry and let the kubelet pull. Without one, export to an archive and import directly into containerd's Kubernetes namespace:
localhost/ckad-demo:1 sha256:ce4707df091c20a395e7f525074ddacb764e9aa71853551daa4c96d57d278a03Two details matter.
-n k8s.io. containerd namespaces its image store, and the kubelet only looks ink8s.io. Import without it and the image is there, and the kubelet will still say it cannot find it.The tag must match what the Pod asks for, exactly.
localhost/ckad-demo:1in the archive and in the manifest.bash Example session sudo buildah push localhost/ckad-demo:1 docker-archive:/tmp/ckad-demo.tar:localhost/ckad-demo:1 2>&1 | tail -3Copying blob sha256:c20b9e8228e83d7faccb9d270a007e03218c581932827527f41a8f7f15bd232fCopying config sha256:271554bf48cdd66b5c12a8f3bd885fc5ccb27f4ea2d6a7c723f448922fd1a307Writing manifest to image destinationls -lh /tmp/ckad-demo.tar-rw-r--r-- 1 root root 4.5M Aug 23 03:17 /tmp/ckad-demo.tarsudo ctr -n k8s.io images import /tmp/ckad-demo.tarlocalhost/ckad demo:1 savedapplication/vnd.docker.distribution.manifest.v2+json sha256:ce4707df091c20a395e7f525074ddacb764e9aa71853551daa4c96d57d278a03Importing elapsed: 0.3 s total: 0.0 B (0.0 B/s)sudo ctr -n k8s.io images ls | grep ckad-demo | awk '{print $1, $3}'localhost/ckad-demo:1 sha256:ce4707df091c20a395e7f525074ddacb764e9aa71853551daa4c96d57d278a03Expected resultA 4.5MB archive imported and listed under its full tag.
Success conditionThe kubelet can now run an image that exists nowhere but this node.
-
Run it
The Pod names the image and sets
imagePullPolicy: Never:NAME READY STATUS RESTARTS AGE mine 0/1 Completed 0 12sbuilt on the node / arg=default-argumentBoth defaults came from the image - the
ENVand theCMD.imagePullPolicy: Neveris required here. The default policy for a tag that is not:latestisIfNotPresent, which would also work; but if the tag were:latestthe default becomesAlways, the kubelet would try to pulllocalhost/ckad-demofrom a registry, and the Pod would sit inErrImagePullwith a locally present image. That is a genuinely confusing failure and the reason to avoid:latestfor anything you build.bash Example session kubectl create namespace ckad-imgnamespace/ckad-img createdsleep 12; kubectl -n ckad-img get pod mineNAME READY STATUS RESTARTS AGEmine 0/1 Completed 0 12skubectl -n ckad-img logs minebuilt on the node / arg=default-argumentExpected result
Completed, printing the image's own defaults.Success conditionYou ran an image you built, with no registry in the path.
-
Modify it without rebuilding
This is the half CKAD actually asks about. Same image, no rebuild - the manifest supplies
argsandenv:overridden in the Pod / arg=from-the-manifestBoth defaults replaced. The mapping worth memorising:
| Dockerfile / Containerfile | Pod spec | Effect | |---|---|---| |
ENTRYPOINT|command| the executable | |CMD|args| its arguments | |ENV|env| environment |The trap is setting
commandwhen you meantargs. The image's ENTRYPOINT is then discarded entirely and your value has to be a complete executable - which is whycommand: ["--verbose"]producesexec: "--verbose": executable file not found.So when a task says "run this image but pass it these arguments", the answer is
args. Almost alwaysargs.bash Example session sleep 12; kubectl -n ckad-img logs overriddenoverridden in the Pod / arg=from-the-manifestkubectl delete namespace ckad-img --wait=falsenamespace "ckad-img" deletedExpected resultBoth the greeting and the argument replaced from the manifest.
Success conditionYou can change what an image does without touching the image.
Troubleshooting
ErrImagePullfor an image you imported to the node.Why: Imported into the wrong containerd namespace, or the tag is
:latestso the policy defaults toAlwaysand the kubelet tries to pull.Fix:Import with
-n k8s.io, avoid:latest, and setimagePullPolicy: Never.The image runs on one node and fails on another.
Why: It was imported to a single node's runtime.
Fix:Import on every node, use a registry, or pin the Pod to the node that has it.
exec: "...": executable file not found.Why:
commandwas set whereargswas meant, discarding the image's ENTRYPOINT.Fix:Use
argsto pass arguments. Usecommandonly to replace the executable.An env var set in the image does not appear.
Why: A Pod-level
enventry with the same name overrides it.Fix:
kubectl exec <pod> -- envshows what the process actually got.