Sitemap
Nadun Kumarasinghe

Nadun Kumarasinghe

CKAD in 30 Days | Day 1: Container Images

7 min readOct 31, 2026

--

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.0
  • docker build -t <name>:<tag> <context> reads a Dockerfile in the context directory and produces an image.
  • docker tag adds a second name to the same image (useful when you want to tag both 1.0 and latest).
  • docker push uploads 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.0

The 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=P

Reference it in the Pod spec:

spec:
imagePullSecrets:
- name: regcred
containers:
- name: app
image: REG/repo/app:1.0

One 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:

  • nginx or nginx:latest → defaults to Always
  • nginx:1.25 or 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: IfNotPresent

Valid 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"]
  • command replaces Dockerfile ENTRYPOINT.
  • args replaces Dockerfile CMD.

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

Using 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.tar

Verify:

file /root/cloud-3.0.tar
tar -tf /root/cloud-3.0.tar | head

You 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-docker

Podman 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-podman

Verify both are in the registry:

curl -s http://registry.killer.sh:5000/v2/sun-cipher/tags/list

Why 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/Dockerfile

Replace it:

sed -i 's|^ENV SUN_CIPHER_ID.*|ENV SUN_CIPHER_ID 5e9c1065-e39d-4a43-a04a-e59bcea3e03f|' \
/opt/course/11/image/Dockerfile

Then 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:v1

Why 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-podman

Verify it is running:

podman ps

Capture logs:

bash

podman logs sun-cipher > /opt/course/11/logs

Capture the container list:

podman ps > /opt/course/11/containers

Why 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 prod

Create the Secret:

kubectl -n prod create secret docker-registry regcred \
--docker-server=myreg.io \
--docker-username=teamuser \
--docker-password=t3am-pass

Create 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.yaml

Edit 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: Never

Apply:

kubectl apply -f app.yaml

Verify the Pod can pull the image:

kubectl -n prod get pod app -w

If 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 save produces a Docker archive, not an OCI archive. If the task says “OCI archive,” use podman save --format oci-archive or skopeo copy … oci-archive:….
  • docker needs sudo on the exam environment unless the task says otherwise. Podman does not. Read the task carefully.
  • The imagePullPolicy default changes based on the tag. latest and no-tag both default to Always. A specific tag defaults to IfNotPresent.
  • command and args are 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 default does nothing for Pods in prod.

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.

--

--