CertGrid CertGrid
Hands-on Lab·Certified Kubernetes Application Developer

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

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.

Runs on the single-node cka4001, because it installs a package on the host and imports an image into that node's containerd. Both are host-level changes, and on a multi-node cluster the image would only exist on the one node you imported it to.
Server NameIP AddressOSRolesCPURAMHDD
CKA4001192.168.0.191Ubuntu 26.04 LTSSingle Node (control plane, untainted)2 Core4 GB50 GB

Before you start

  1. 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, an ENTRYPOINT and a CMD. 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:1271554bf48cdd66b5c12a8f3bd885fc5ccb27f4ea2d6a7c723f448922fd1a307

    Expected 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.

  2. 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:ce4707df091c20a395e7f525074ddacb764e9aa71853551daa4c96d57d278a03

    Two details matter.

    -n k8s.io. containerd namespaces its image store, and the kubelet only looks in k8s.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:1 in 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:ce4707df091c20a395e7f525074ddacb764e9aa71853551daa4c96d57d278a03

    Expected 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.

  3. Run it

    The Pod names the image and sets imagePullPolicy: Never:

    NAME   READY   STATUS      RESTARTS   AGE
    mine   0/1     Completed   0          12s
    built on the node / arg=default-argument

    Both defaults came from the image - the ENV and the CMD.

    imagePullPolicy: Never is required here. The default policy for a tag that is not :latest is IfNotPresent, which would also work; but if the tag were :latest the default becomes Always, the kubelet would try to pull localhost/ckad-demo from a registry, and the Pod would sit in ErrImagePull with a locally present image. That is a genuinely confusing failure and the reason to avoid :latest for 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-argument

    Expected resultCompleted, printing the image's own defaults.

    Success conditionYou ran an image you built, with no registry in the path.

  4. Modify it without rebuilding

    This is the half CKAD actually asks about. Same image, no rebuild - the manifest supplies args and env:

    overridden in the Pod / arg=from-the-manifest

    Both 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 command when you meant args. The image's ENTRYPOINT is then discarded entirely and your value has to be a complete executable - which is why command: ["--verbose"] produces exec: "--verbose": executable file not found.

    So when a task says "run this image but pass it these arguments", the answer is args. Almost always args.

    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" deleted

    Expected resultBoth the greeting and the argument replaced from the manifest.

    Success conditionYou can change what an image does without touching the image.

Troubleshooting

Official sources