Introduction
Almost every CKAD candidate spends their study time on YAML — Deployments, Services, Pods — and treats container images as someone else’s job. That’s a mistake. The very first sub-objective of the Application Design and Build domain, which is worth 20% of the Certified Kubernetes Application Developer (CKAD) exam, is “Define, build and modify container images.” The exam expects you to be comfortable writing a Dockerfile, building an image, tagging it correctly, and getting it running inside a Pod — all under time pressure.
This is the part of the exam that connects “I wrote some application code” to “Kubernetes is running it.” If you can’t build and tag an image confidently, you’ll stumble on tasks that look trivial on paper. The good news: the image-building skills the CKAD tests are a small, well-defined set. You don’t need to be a Docker expert. You need to know the Dockerfile instructions cold, understand how layers and tags work, and be able to slim an image down when asked.
This guide walks through everything in the “build and modify container images” objective the way the exam frames it: image anatomy, the Dockerfile instruction set, building with Docker/Podman/Buildah, tagging, multi-stage builds, and shrinking image size. By the end you’ll be able to take a directory of source code and turn it into a running Pod without hesitation. Pair it with the CKAD exam domains breakdown to see exactly where this fits in the broader curriculum.
Why Container Images Matter on the CKAD
The CKAD is a hands-on, performance-based exam. You’re dropped into a real terminal with kubectl and a container runtime, and you solve tasks against live clusters. Several of those tasks involve images directly:
- Writing or fixing a Dockerfile so an image builds successfully.
- Building an image from provided source and tagging it for a local or private registry.
- Modifying an existing image (changing the base, the command, exposed ports, or environment).
- Deploying that image into a Pod or Deployment and confirming it runs.
Crucially, the cluster on the exam pulls images the way a real cluster does — so if you tag an image wrong, or forget to make it available to the node, your Pod sits in ImagePullBackOff and you lose points. Understanding the full lifecycle, from build to a running Pod, is what separates a confident candidate from one who panics.
A note on tooling: modern Kubernetes nodes use containerd or CRI-O rather than Docker, and many exam environments ship Podman and Buildah instead of the Docker CLI. The good news is the commands are nearly identical. Wherever you see docker build, you can almost always substitute podman build or buildah bud. Learn the concepts, not just one binary.
Anatomy of a Container Image
Before writing a Dockerfile, understand what you’re producing. A container image is a read-only, layered filesystem plus metadata (the command to run, environment variables, exposed ports, the default user). Each instruction in a Dockerfile that changes the filesystem creates a new layer, and layers are cached and shared between images.
┌─────────────────────────────┐
│ Metadata (CMD, ENV, PORTS) │
├─────────────────────────────┤
│ Layer: COPY app code │ ← your code
│ Layer: RUN pip install │ ← dependencies
│ Layer: base image (python) │ ← OS + runtime
└─────────────────────────────┘
Two consequences of this layering matter for the exam:
- Order affects caching and size. Instructions that change rarely (installing dependencies) should come before instructions that change often (copying your source). That way a code change doesn’t invalidate the dependency layer, and rebuilds are fast.
- Layers only add. Deleting a file in a later layer doesn’t reclaim its space — the data still exists in the earlier layer. This is why squashing work into a single
RUN, or using multi-stage builds, matters for image size.
When a container starts, the runtime adds a thin writable layer on top of the read-only image layers. Anything written there is lost when the container is removed — which is exactly why Kubernetes uses Volumes for persistence. The CKAD expects you to connect these dots: images are immutable; state lives elsewhere.
The Dockerfile Instruction Set
A Dockerfile is a recipe. The CKAD won’t ask you to write an elaborate one, but you must know what each instruction does and when to use it. Here are the instructions worth memorizing:
| Instruction | Purpose | Example |
|---|---|---|
FROM | Base image to build on | FROM python:3.12-slim |
WORKDIR | Set the working directory | WORKDIR /app |
COPY | Copy files from build context into the image | COPY . . |
ADD | Like COPY, but also handles URLs and tar extraction | ADD app.tar.gz /app |
RUN | Execute a command at build time (new layer) | RUN pip install -r requirements.txt |
ENV | Set an environment variable | ENV PORT=8080 |
ARG | Build-time variable (not present at runtime) | ARG VERSION=1.0 |
EXPOSE | Document the port the app listens on | EXPOSE 8080 |
USER | Run as a non-root user | USER 1000 |
ENTRYPOINT | The executable that always runs | ENTRYPOINT ["python"] |
CMD | Default arguments (or command) at run time | CMD ["app.py"] |
The two that trip people up are ENTRYPOINT vs CMD and RUN vs CMD:
RUNhappens at build time and bakes its result into a layer.CMD/ENTRYPOINTdefine what happens at run time.ENTRYPOINTsets the fixed executable;CMDprovides default arguments that are easy to override. A common pattern isENTRYPOINT ["python"]+CMD ["app.py"], which runspython app.pybut lets you override the script.
Prefer the exec form (CMD ["python", "app.py"]) over the shell form (CMD python app.py). The exec form runs your process as PID 1 directly, so it receives signals like SIGTERM correctly — which matters when Kubernetes terminates a Pod gracefully.
Here’s a complete, exam-realistic Dockerfile for a Python web app:
FROM python:3.12-slim
WORKDIR /app
# Dependencies first so this layer is cached across code changes
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Then the application code
COPY . .
ENV PORT=8080
EXPOSE 8080
USER 1000
ENTRYPOINT ["python"]
CMD ["app.py"]
Building an Image
With a Dockerfile in place, you build the image. The build sends the build context (the directory you point at, usually .) to the builder, which executes each instruction.
# Docker
docker build -t myapp:1.0 .
# Podman (drop-in replacement)
podman build -t myapp:1.0 .
# Buildah
buildah bud -t myapp:1.0 .
The -t flag tags the image as myapp:1.0. The trailing . is the build context. To verify the build succeeded and inspect what you produced:
docker images # list images and sizes
docker history myapp:1.0 # see the layers and how big each is
docker inspect myapp:1.0 # full metadata: CMD, ENV, exposed ports
docker history is the single most useful command for the “modify an image to make it smaller” style of task — it shows you exactly which layer is bloating your image.
Use a .dockerignore file to keep junk out of the build context (and out of your image). It works like .gitignore:
.git
node_modules
*.log
__pycache__
This both speeds up builds and avoids accidentally copying secrets or huge directories into the image.
Tags and the latest Trap
A tag is how you name and version an image. The full reference looks like:
registry.example.com/team/myapp:1.0
└──── registry ─────┘└─repo─┘└tag┘
If you omit the registry, the runtime assumes Docker Hub. If you omit the tag, it defaults to latest — and latest is a trap on the exam and in production. It’s just a string; it doesn’t mean “newest.” Worse, if a Pod uses image: myapp:latest, the default imagePullPolicy becomes Always, so the kubelet tries to re-pull on every start. In an exam environment where the image only exists locally on the node, that re-pull can fail with ImagePullBackOff.
Always tag with an explicit version and reference that exact tag in your Pod spec. To add a new tag to an existing image:
docker tag myapp:1.0 registry.example.com/team/myapp:1.0
docker push registry.example.com/team/myapp:1.0
When you then write the Pod, set the policy explicitly so the kubelet uses what’s already on the node:
spec:
containers:
- name: myapp
image: myapp:1.0
imagePullPolicy: IfNotPresent
IfNotPresent tells the kubelet to use the local image if it exists — exactly what you want when you’ve just built it on the node. This single detail resolves a large fraction of “my Pod won’t start” moments on the CKAD.
Multi-Stage Builds
Multi-stage builds are the most important advanced image skill on the CKAD, because they’re how you ship a small, secure image. The idea: use one stage with all your build tools (compilers, dev dependencies) to produce an artifact, then copy only that artifact into a clean, minimal final stage. The build tooling never ships.
# Stage 1: build
FROM golang:1.22 AS builder
WORKDIR /src
COPY . .
RUN CGO_ENABLED=0 go build -o /bin/app ./cmd/app
# Stage 2: runtime — only the compiled binary ships
FROM gcr.io/distroless/static
COPY --from=builder /bin/app /app
ENTRYPOINT ["/app"]
The key line is COPY --from=builder, which pulls the artifact from the earlier stage. The final image contains the Go binary and almost nothing else — no compiler, no shell, no package manager. A naive single-stage Go image might be 800 MB; the multi-stage version can be under 10 MB. Smaller images pull faster, start faster, and have a dramatically smaller attack surface — which is why this pattern also appears in security-focused certs.
You’ll see multi-stage builds in CKAD tasks phrased as “reduce the size of this image” or “the final image must not contain build tools.” Recognize the pattern and apply it.
Reducing Image Size
When a task asks you to slim an image, you have a small toolbox:
-
Pick a smaller base.
python:3.12-sliminstead ofpython:3.12;node:20-alpineinstead ofnode:20;distrolessorscratchfor compiled languages. The base image is usually the biggest single contributor. -
Use multi-stage builds to leave build dependencies behind (above).
-
Combine
RUNinstructions and clean up in the same layer. Because layers only add, cleaning up in a later layer doesn’t help:RUN apt-get update \ && apt-get install -y --no-install-recommends curl \ && rm -rf /var/lib/apt/lists/* -
Use
--no-cache-dirfor package managers (pip) and--no-install-recommendsfor apt to avoid pulling optional extras. -
Add a
.dockerignoreso you don’t copy.git, test fixtures, ornode_modulesinto the image.
The Alpine-based images are tiny but use musl libc instead of glibc, which occasionally breaks binaries that expect glibc. Know the trade-off; the exam may give you a base image and you should be able to reason about why a build fails.
Modifying an Existing Image
“Modify a container image” usually means: change the base image, the command, the exposed port, an environment variable, or the user — then rebuild. The workflow is always the same:
- Edit the Dockerfile.
- Rebuild with a new tag (
myapp:1.1) so you don’t clobber the old one. - Update the Pod/Deployment to reference the new tag.
For Deployments, the imperative shortcut to roll out a new image is worth memorizing for speed:
kubectl set image deployment/myapp myapp=myapp:1.1 --record
kubectl rollout status deployment/myapp
This connects image-building to the Application Deployment domain — once your new image is built and tagged, kubectl set image triggers a rolling update. For the full mechanics of that rollout, see the CKAD Deployments & rolling updates guide.
From Image to Running Pod
Tie it all together with the fastest possible path from a built image to a verified Pod. Generate the manifest imperatively rather than writing YAML by hand:
# Generate a Pod manifest without creating it
kubectl run myapp --image=myapp:1.0 --port=8080 \
--dry-run=client -o yaml > pod.yaml
# Edit pod.yaml if needed (add imagePullPolicy: IfNotPresent), then:
kubectl apply -f pod.yaml
kubectl get pod myapp -w # watch it reach Running
kubectl logs myapp # confirm the app started
If the Pod is stuck, kubectl describe pod myapp tells you why. The two image-related failures to recognize instantly:
| Status | Likely cause | Fix |
|---|---|---|
ImagePullBackOff | Wrong tag, image not on node, or registry auth missing | Fix the tag; set imagePullPolicy: IfNotPresent; add an imagePullSecret |
ErrImagePull | Image name typo or registry unreachable | Correct the image reference |
CrashLoopBackOff | Image runs but the process exits | Check CMD/ENTRYPOINT and kubectl logs |
Diagnosing these quickly is a core skill — the CKAD application troubleshooting guide drills the full workflow.
Speed Tips for Exam Day
The CKAD gives you roughly two hours for a stack of tasks, so efficiency on image work pays off:
- Alias your build tool. If the environment uses Podman,
alias docker=podmanso muscle memory still works. - Use
--dry-run=client -o yamlto scaffold Pod manifests instead of hand-writing them. - Default to
imagePullPolicy: IfNotPresentfor locally built images to avoid pull failures. - Tag explicitly, never rely on
latest. docker historyto find the bloated layer when asked to shrink an image.- Keep a
.dockerignoreinstinct — it prevents a whole class of “why is my image huge / why did my secret leak in” problems.
Practicing these until they’re reflexive is the whole game. The CKAD Mock Exam Bundle puts you in a live, browser-based terminal with performance-based tasks — including image build-and-deploy scenarios — so you build the same muscle memory you’ll need on exam day, with detailed explanations for every solution. Combine it with the CKAD study plan to sequence your prep.
Frequently Asked Questions
Do I need to know Docker for the CKAD?
You need to understand container images and Dockerfiles, but not Docker specifically. Many exam environments use Podman and Buildah, whose commands mirror Docker’s (podman build, buildah bud). Learn the concepts — layers, tags, multi-stage builds — and the specific CLI matters far less.
Will the CKAD ask me to write a full Dockerfile from scratch?
Usually you’ll be given a partial Dockerfile or source code and asked to complete, fix, or modify it — change the base image, add a build step, set the command, or shrink the result. Knowing the instruction set (FROM, RUN, COPY, CMD, ENTRYPOINT, USER) cold lets you do this in a minute or two.
Why does my Pod show ImagePullBackOff after I built the image?
Most often because the Pod references latest (forcing a re-pull) or a tag the node can’t find. Build with an explicit tag, reference that exact tag in the Pod, and set imagePullPolicy: IfNotPresent so the kubelet uses the local image instead of trying to pull it.
What’s the difference between CMD and ENTRYPOINT?
ENTRYPOINT defines the executable that always runs; CMD provides default arguments that are easy to override at runtime. Use them together (ENTRYPOINT ["python"] + CMD ["app.py"]) when you want a fixed program with overridable arguments. Prefer the exec (JSON array) form so your process runs as PID 1 and receives signals.
How do I make a container image smaller for an exam task?
Switch to a slimmer base (-slim, -alpine, or distroless), use a multi-stage build to leave build tools behind, combine RUN steps with cleanup in the same layer, and add a .dockerignore. Use docker history to find which layer is the largest before you start cutting.
Conclusion
The “define, build and modify container images” objective is small but foundational — it’s the bridge between application code and a running Pod, and it shows up across the Application Design, Build, and Deployment domains. Master the Dockerfile instruction set, understand layers and tags, internalize multi-stage builds for slim images, and always tag explicitly with imagePullPolicy: IfNotPresent for locally built images. Get these reflexes solid and the image tasks become some of the fastest points on the exam.
From here, continue with the CKAD exam domains overview to see how image-building connects to deployments and configuration, brush up on Jobs and CronJobs for the rest of the Application Design domain, and then prove your reflexes are automatic in the live terminal of the CKAD Mock Exam Bundle — because the candidate who can go from Dockerfile to running Pod in under five minutes is the one who finishes the exam with time to spare.