CKAD in 30 Days | Day 1: Container Images
Build, export, push, and reference container images the way the exam actually tests it
This is Day 1 of CKAD in 30 Days.
The CKAD exam is hands-on. You will not be asked “what is a container image.” You will be given a Dockerfile at a specific path, told to build it with a specific name and tag, and told to export it to a specific tarball path. The exam measures whether you can finish the task without pushing to a registry you were told not to push to.
So this chapter walks through what you need to actually do that under time pressure.
The lesson
1. What a container image actually is
An image is a filesystem bundle plus some metadata (what command to run, what ports to expose, what environment variables to set). It is immutable. Build it once, run it anywhere.
The CKAD exam does not test container internals. It tests whether you can:
- build an image from a Dockerfile,
- export it to a tarball or push it to a registry,
- reference it correctly from a Pod,
- override its default command,
- control when Kubernetes pulls it.
2. Build with Docker
The classic path. Three commands:
docker build -t repo/app:1.0 .
docker tag repo/app:1.0 repo/app:latest
docker push repo/app:1.0docker build -t <name>:<tag> <context>reads a Dockerfile in the context directory and produces an image.docker tagadds a second name to the same image (useful when you want to tag both1.0andlatest).docker pushuploads to a registry Kubernetes can read.
3. Build with Podman
Podman is a drop-in replacement for Docker. Same commands, same flags.
podman build -t repo/app:1.0 .
podman push repo/app:1.0The practical difference you hit on day one is permissions:
# Fresh Docker install - typically needs sudo
sudo docker build -t repo/app:1.0 .
# After: sudo usermod -aG docker $USER (log out, log back in)
docker build -t repo/app:1.0 .# Podman - rootless by default, no setup needed
podman build -t repo/app:1.0 .
Docker runs through a daemon owned by root:docker. Your user either uses sudo or joins the docker group. Podman is daemonless and runs as your user by default.
The CKAD exam does not care which tool you used. The resulting image is identical.
4. Export an image to a tarball (OCI format)
The exam often asks you to save the image to a specific tarball instead of pushing to a registry. The format is OCI archive. All three major tools support it
# Docker
docker save -o /path/to/app.tar repo/app:1.0# Podman - OCI archive
podman save --format oci-archive -o /path/to/app.tar repo/app:1.0# skopeo - the most flexible tool for image transport
skopeo copy containers-storage:repo/app:1.0 oci-archive:/path/to/app.tar
If the task says “OCI archive format,” use --format oci-archive on Podman or skopeo with an oci-archive: destination. Default docker save produces a Docker archive, not OCI. That difference has failed candidates.
5. Private registry: pull secret
If your Pod needs to pull from a private registry, Kubernetes needs credentials.
kubectl create secret docker-registry regcred \
--docker-server=REG \
--docker-username=U \
--docker-password=PReference it in the Pod spec:
spec:
imagePullSecrets:
- name: regcred
containers:
- name: app
image: REG/repo/app:1.0One imagePullSecrets entry per registry. The container references the image by its full path.
6. imagePullPolicy
Kubernetes decides when to pull based on the image tag:
nginxornginx:latest→ defaults to Alwaysnginx:1.25or any specific tag → defaults to IfNotPresent- A digest like
nginx@sha256:…→ defaults to IfNotPresent
Rule in one sentence: if the image moves (no tag, or latest), Kubernetes pulls every time. If it is pinned, Kubernetes caches.
Override explicitly:
containers:
- name: app
image: nginx:1.25
imagePullPolicy: IfNotPresentValid values: Always, IfNotPresent, Never.
7. Override the image command
Sometimes you need the container to run a different command than the image’s default. Kubernetes maps these to the Dockerfile fields:
yaml
spec:
containers:
- name: app
image: busybox
command: ["sh", "-c"]
args: ["echo hello"]commandreplaces DockerfileENTRYPOINT.argsreplaces DockerfileCMD.
Easy to mix up. Memorize: command is the executable, args is its parameters.
Quick reference card
# Build and save (no push)
docker build -t repo/app:1.0 .
docker save -o /path/app.tar repo/app:1.0# Podman with OCI archive export
podman build -t repo/app:1.0 .
podman save --format oci-archive -o /path/app.tar repo/app:1.0# skopeo - OCI archive from local containers-storage
skopeo copy containers-storage:repo/app:1.0 oci-archive:/path/app.tar# Create a pull secret
kubectl create secret docker-registry regcred \
--docker-server=REG --docker-username=U --docker-password=P# Attach pull secret to default ServiceAccount
kubectl patch sa default -p '{"imagePullSecrets":[{"name":"regcred"}]}'# Quick-generate a Pod YAML overriding command/args
kubectl run app --image=busybox --restart=Never \
--dry-run=client -o yaml -- sh -c 'echo hello' > pod.yaml
Practice tasks
All exam-shape. Specific paths. Specific outputs. Explicit constraints. Try each one before reading the solution.
Task 1 — Build and export to OCI archive
A Dockerfile is at /root/app-source/Dockerfile.
Build a container image named cloud:3.0 using the tool of your choice (docker, podman, skopeo, or img).
Export the built image in OCI archive format to /root/cloud-3.0.tar.
Do not push to any registry. Do not run the image.
Solution
Get Nadun Kumarasinghe’s stories in your inbox
Join Medium for free to get updates from this writer.
Using Podman (recommended for the exam):
cd /root/app-source
podman build -t cloud:3.0 .
podman save --format oci-archive -o /root/cloud-3.0.tar cloud:3.0Using Docker (if that is what is available):
cd /root/app-source
sudo docker build -t cloud:3.0 .# Docker's "save" produces a Docker archive, not OCI. Convert with skopeo:
sudo skopeo copy docker-daemon:cloud:3.0 oci-archive:/root/cloud-3.0.tarVerify:
file /root/cloud-3.0.tar
tar -tf /root/cloud-3.0.tar | headYou should see entries like oci-layout, index.json, blobs/sha256/….
Why this is the exam shape: the task names a specific build context, a specific image name and tag, a specific output path, and an explicit “do not push” constraint. Hitting all four is what passes the task.
Task 2 — Build the same image with two tools, push both
A Dockerfile is at /opt/course/11/image/Dockerfile.
Build it twice. Once with Docker tagged registry.killer.sh:5000/sun-cipher:v1-docker. Once with Podman tagged registry.killer.sh:5000/sun-cipher:v1-podman.
Push both to the registry.
Run all docker commands as root with sudo. Run podman commands as the candidate user.
Solution
Docker side (needs sudo):
cd /opt/course/11/image
sudo docker build -t registry.killer.sh:5000/sun-cipher:v1-docker .
sudo docker push registry.killer.sh:5000/sun-cipher:v1-dockerPodman side (runs as candidate):
cd /opt/course/11/image
podman build -t registry.killer.sh:5000/sun-cipher:v1-podman .
podman push registry.killer.sh:5000/sun-cipher:v1-podmanVerify both are in the registry:
curl -s http://registry.killer.sh:5000/v2/sun-cipher/tags/listWhy this is the exam shape: the task mixes two tools, two tags, two auth contexts (sudo vs user), and a specific registry URL. The exam uses this pattern to test whether you confuse Docker and Podman commands or use the wrong user for each.
Task 3 — Change a Dockerfile value, then build
A Dockerfile is at /opt/course/11/image/Dockerfile.
Change the value of the environment variable SUN_CIPHER_ID inside that Dockerfile to the hardcoded value 5e9c1065-e39d-4a43-a04a-e59bcea3e03f.
Then build the image with Podman as registry.killer.sh:5000/sun-cipher:v1 and push it.
Solution
Edit the Dockerfile first. Find the existing ENV SUN_CIPHER_ID … line:
grep SUN_CIPHER_ID /opt/course/11/image/DockerfileReplace it:
sed -i 's|^ENV SUN_CIPHER_ID.*|ENV SUN_CIPHER_ID 5e9c1065-e39d-4a43-a04a-e59bcea3e03f|' \
/opt/course/11/image/DockerfileThen build and push:
cd /opt/course/11/image
podman build -t registry.killer.sh:5000/sun-cipher:v1 .
podman push registry.killer.sh:5000/sun-cipher:v1Why this is the exam shape: the task hides a file-edit step inside what looks like a build task. If you jump straight to podman build without reading the full task, you build the wrong image and lose the points.
Task 4 — Run a container from a built image
Use the image registry.killer.sh:5000/sun-cipher:v1-podman from Task 2.
Run a container from that image as the candidate user. The container must be named sun-cipher and must keep running in the background.
Write the container’s logs into /opt/course/11/logs.
Write a list of all running Podman containers into /opt/course/11/containers.
Solution
Run the container detached and named:
podman run -d --name sun-cipher registry.killer.sh:5000/sun-cipher:v1-podmanVerify it is running:
podman psCapture logs:
bash
podman logs sun-cipher > /opt/course/11/logsCapture the container list:
podman ps > /opt/course/11/containersWhy this is the exam shape: the task is about shell plumbing as much as Podman. You must know that > redirects to a file, that podman logs reads the container’s stdout, and that podman ps lists only running containers (use -a if you also need stopped ones).
Task 5 — Create a Pod that pulls from a private registry
A private image is at myreg.io/corp/app:2.0. The registry requires authentication with username teamuser and password t3am-pass.
Create a Secret named regcred in namespace prod that holds those credentials.
Create a Pod named app in namespace prod that runs the image myreg.io/corp/app:2.0 and uses the Secret to pull it.
Solution
Create the namespace if needed:
kubectl create ns prodCreate the Secret:
kubectl -n prod create secret docker-registry regcred \
--docker-server=myreg.io \
--docker-username=teamuser \
--docker-password=t3am-passCreate the Pod. Fastest path is to generate a stub, then edit:
kubectl -n prod run app --image=myreg.io/corp/app:2.0 \
--restart=Never --dry-run=client -o yaml > app.yamlEdit app.yaml to add imagePullSecrets under spec:
apiVersion: v1
kind: Pod
metadata:
name: app
namespace: prod
spec:
imagePullSecrets:
- name: regcred
containers:
- name: app
image: myreg.io/corp/app:2.0
restartPolicy: NeverApply:
kubectl apply -f app.yamlVerify the Pod can pull the image:
kubectl -n prod get pod app -wIf the Pod stays in ImagePullBackOff, re-check the Secret’s registry URL and credentials with kubectl -n prod describe pod app.
Why this is the exam shape: three resources (namespace, Secret, Pod), two of them cross-referenced. The exam wants to see you connect them with the right field (imagePullSecrets).
Gotchas you would hit in the exam
docker saveproduces a Docker archive, not an OCI archive. If the task says “OCI archive,” usepodman save --format oci-archiveorskopeo copy … oci-archive:….dockerneedssudoon the exam environment unless the task says otherwise. Podman does not. Read the task carefully.- The
imagePullPolicydefault changes based on the tag.latestand no-tag both default toAlways. A specific tag defaults toIfNotPresent. commandandargsare easy to swap. Command is the executable. Args is what you pass to it. Mixing them silently runs the Dockerfile’s default and your “override” never fires.- A pull secret is per-namespace. Creating it in
defaultdoes nothing for Pods inprod.
What is next
Tomorrow, Day 2: picking the right workload resource. Deployment vs StatefulSet vs DaemonSet vs Job vs CronJob: which one to reach for and how to create each one fast.
If you want to follow the carousel version of these lessons, I post one per day on LinkedIn as part of Kubestronaut in 100 Days. The link is at the top of my profile.









