Certified Kubernetes Application Developer (CKAD) #4 Container Images: Dockerfile, Multi-stage, and Building from Scratch on the Exam
If #3 Multi-container patterns dealt with the design of placing several containers inside a single Pod, this post drops down to the raw material of those containers — building the image yourself. CKAD isn’t an exam where you only write manifests. Some tasks demand the whole flow at once: build an image from given source, push it to a registry, and then run a Pod that references it.
So this post covers one full cycle of building and referencing an image from a hands-on exam angle — from the basic Dockerfile instructions to multi-stage builds, the podman/buildah build procedures common in the exam environment, and the imagePullPolicy trap. If you’re comfortable with Docker, most of it carries over directly, but knowing the differences in the exam environment is the key.
Why CKAD has you build images yourself #
CKAD’s first domain is Application Design and Build. As the name says, it includes the ability to build an application, so the exam can throw at you a task like “build an image from the Dockerfile in the given directory, tag it myapp:1.0, push it to the local registry, and then run a Pod with that image.”
What makes this task tricky is that a single task mixes together the build tool, tagging, pushing, and the manifest — if even one step goes wrong, the Pod falls into ErrImagePull or ImagePullBackOff. So you need the habit of verifying not just that the build succeeded, but that the image is actually pulled correctly in the cluster. If you’ve gone through the Docker introduction series, the Dockerfile itself will be familiar, but here we compress it to the parts that translate directly into exam points.
Basic Dockerfile instructions #
An image is built by running the instructions in a text file called Dockerfile from top to bottom. Each instruction creates one layer, and these layers stack up into the final image.
FROM node:20-alpine # base image (always the first instruction)
WORKDIR /app # base path for the following instructions
COPY package*.json ./ # copy only the dependency files first (cache optimization)
RUN npm ci --omit=dev # run a shell command at build time
COPY . . # copy the rest of the source
EXPOSE 3000 # document the exposed port (does not actually open it)
CMD ["node", "server.js"] # default command at startup (overridable)RUN executes a shell command at build time, while CMD and ENTRYPOINT define what to run when the container starts. The distinction between these two is the key.
The difference between CMD and ENTRYPOINT #
The difference between these two leads to exam questions about command/args mapping, so you need to know it precisely.
ENTRYPOINTfixes what the container runs as. Thedocker runarguments or the Pod’sargsare appended after it.CMDis the default arguments or default command. If you provide arguments at runtime, it is replaced wholesale.
The recommended pattern is the combination of fixing the executable with ENTRYPOINT and giving default arguments with CMD.
ENTRYPOINT ["python", "app.py"]
CMD ["--port", "8080"]Set up this way, the default run is python app.py --port 8080, and passing --port 9090 at runtime gives python app.py --port 9090.
Layer cache: instruction order decides build speed #
A build reuses unchanged layers from the cache. When some layer changes, all layers below it are rebuilt. So, as in the example above, copying and installing the rarely changing dependency files (COPY package*.json) before the frequently changing source (COPY . .) means that when you only edit the source, the dependency-install layer is reused and the build is faster.
When build time grows on the exam, you lose that much time, so knowing the order that keeps the cache effective is an advantage in practice.
Slimming images with multi-stage builds #
The tools needed for the build (compilers, package managers) aren’t needed at runtime. Yet if you build in a single stage, these tools stay in the final image and make it heavy. A multi-stage build separates the build stage from the runtime stage, copying only the artifacts into the runtime stage.
Go example: build stage + distroless runtime #
# build stage: produce the binary with the Go compiler
FROM golang:1.22 AS builder
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /app/server ./cmd/server
# runtime stage: a minimal image without even a shell
FROM gcr.io/distroless/static-debian12
COPY --from=builder /app/server /server
EXPOSE 8080
ENTRYPOINT ["/server"]The key is naming a stage with FROM ... AS builder and, in the final stage, pulling in only the artifacts of the previous stage with COPY --from=builder. Since the first stage that held the Go compiler isn’t included in the final image, a several-hundred-MB image shrinks to tens of MB. For Node, you apply the same pattern by building the bundle with npm run build in the build stage and then copying only dist and node_modules into the runtime stage’s node:20-alpine. Because the build tools and source code don’t end up in the final image, you benefit not only from a smaller image size but also from a reduced security attack surface.
Build, tag, push #
Now the procedure for building the image from the Dockerfile you made and pushing it to a registry. Depending on the exam environment, the build tool may be podman or buildah rather than Docker. You need to be able to do the same task with either command, so I’ll cover both.
Build, tag, push with podman #
podman has high command compatibility with docker, so you can carry commands over almost as-is. The build → tag → push order is as follows.
podman build -t myapp:1.0 . # build
podman tag myapp:1.0 registry.example.com/team/myapp:1.0 # tag with the registry path
podman push registry.example.com/team/myapp:1.0 # push
podman images # check local imagesbuildah and docker side by side #
buildah is a tool specialized for image building; you do Dockerfile builds with buildah bud. In an environment with docker installed, the docker commands work as-is too.
# buildah (bud = build-using-dockerfile)
buildah bud -t myapp:1.0 .
buildah push myapp:1.0 docker://registry.example.com/team/myapp:1.0
# docker
docker build -t myapp:1.0 .
docker tag myapp:1.0 registry.example.com/team/myapp:1.0
docker push registry.example.com/team/myapp:1.0For all three tools, the build -t <name:tag> . → push flow is identical. On the exam, following the tool and tag that the question specifies, exactly as written, is the premise for scoring.
Image references and imagePullPolicy #
Let’s cover how a Pod references the image you built, and the imagePullPolicy that decides when the cluster re-pulls that image.
Tag and digest #
An image is referenced by name:tag (e.g., myapp:1.0) or by name@digest (e.g., myapp@sha256:abc123...). A tag is mutable because you can push different content under the same name again, but a digest is a hash of the image content, so once specified it always points to the same image.
The three imagePullPolicy values #
| Value | Behavior |
|---|---|
Always | Re-pulls from the registry every time |
IfNotPresent | Pulls only when the image isn’t on the node |
Never | Never pulls. The image must already be on the node |
The default depends on the tag. When the tag is :latest or the tag is omitted, the default is Always; for any other specific tag, it’s IfNotPresent.
The latest-tag trap #
:latest is not a special tag that always points to the newest; it’s an ordinary tag that just happens to be named latest. Even so, the default pull policy becomes Always, which can pull images from different points in time on each node, making it hard to track which version is running. That’s why, both in practice and on the exam, it’s safer to specify a concrete version tag on the container, like image: ...myapp:1.0, together with imagePullPolicy: IfNotPresent. For an image you just built locally and that already exists on the node, set Never or IfNotPresent to avoid the situation of failing while trying to pull from a registry that doesn’t have it.
Private registry: imagePullSecrets #
To use an image from a private registry that requires authentication, you create a docker-registry-type Secret holding the credentials and have the Pod reference it.
k create secret docker-registry regcred \
--docker-server=registry.example.com \
--docker-username=ckad \
--docker-password=<password> \
--docker-email=ckad@example.comYou attach the Secret you created to the Pod’s spec.imagePullSecrets.
spec:
imagePullSecrets:
- name: regcred
containers:
- name: app
image: registry.example.com/team/myapp:1.0If you skip this attachment, authentication fails and you get ImagePullBackOff, so for tasks dealing with private images you need to remember Secret creation and the imagePullSecrets attachment as a pair.
Overriding commands in a Pod (an exam regular) #
A Pod manifest’s command and args override the Dockerfile’s ENTRYPOINT and CMD. Questions about this mapping come up often, so I’ll lay it out in a table.
| Pod field | Dockerfile counterpart | Behavior |
|---|---|---|
command | ENTRYPOINT | Overrides the executable |
args | CMD | Overrides the arguments |
That is, specifying command ignores the image’s ENTRYPOINT, and specifying args ignores CMD. Omitting both uses the image’s ENTRYPOINT/CMD as-is.
apiVersion: v1
kind: Pod
metadata:
name: cmd-demo
spec:
containers:
- name: app
image: busybox:1.36
command: ["sh", "-c"] # overrides ENTRYPOINT
args: ["echo hello && sleep 3600"] # overrides CMDWhen creating it imperatively, the tokens after -- follow the mapping above.
k run cmd-demo --image=busybox:1.36 $do \
--command -- sh -c "echo hello && sleep 3600" > pod.yamlHere, if the --command flag is present, what follows -- goes into command; without it, it goes into args. Since this distinction determines whether you override ENTRYPOINT or CMD, you need to read the question text carefully.
Exam points #
- The build tool may not be docker. Get
podman buildandbuildah budinto your fingers so you can do the same task either way. - Tag and push path exactly as in the question. Scoring is based on the specified name, tag, and registry path, so a single typo costs points.
- Multi-stage is
ASandCOPY --from. Memorize the pattern of naming the build stage and copying only the artifacts in the final stage. - imagePullPolicy defaults.
:latestor an omitted tag isAlways, a concrete tag isIfNotPresent. For locally built images, useNever/IfNotPresentto prevent pull failures. - Private registry is the Secret + imagePullSecrets pair. Remember
k create secret docker-registrytogether with the Pod attachment. - command/args mapping.
command→ENTRYPOINT,args→CMD. The presence or absence of the--commandflag decides what you override.
Wrap-up #
What this post locked in:
- CKAD can require one full cycle of building, pushing, and running an image yourself. Not just a single build — verify that the Pod pulls the image correctly.
- Dockerfile basics.
FROM/WORKDIR/COPY/RUN/EXPOSE, plus the difference betweenCMDandENTRYPOINTand the layer-cache order. - Multi-stage builds. Separate the build stage from the runtime stage to slim down with distroless/alpine and reduce the attack surface.
- Build, tag, push. For
podman,buildah, anddocker, thebuild -t→pushflow is identical. - Image references. Tag vs digest, the three
imagePullPolicyvalues and the:latesttrap, andimagePullSecretsfor private registries. - command/args overriding the
ENTRYPOINT/CMDmapping.
Next: Workloads 1 #
We’ve come up to the unit of making an image and running it as a Pod. Now we move up to the workloads that bundle several Pods for operation.
#5 Workloads 1: Deployment, ReplicaSet, rolling update, and rollback covers the principle of maintaining replicas with a ReplicaSet, how to drive a rolling update with a Deployment, the procedure for rolling back with kubectl rollout undo when something goes wrong, and the “roll back to a specific revision” type that comes up often on the exam — all built by hand.