Image Loading and Verification Solutions
SummaryComplete solution for Exercise 3: building a custom...
Complete solution for Exercise 3: building a custom...
Complete solution for Exercise 3: building a custom Docker image, loading it into a Kind cluster, running it as a Pod, and verifying the web server responds.
Image Loading and Verification Solutions
Solution: Exercise 3 — Load a Custom Docker Image into Kind and Run It as a Pod
This exercise chains together several skills: writing a Dockerfile, building an image, loading it into Kind’s container runtime, creating a Pod from it, and verifying the entire pipeline works. Each step has a verification point so you can catch problems early.
Step 1: Create the Project Directory
mkdir -p ~/ckad-lab/custom-image
cd ~/ckad-lab/custom-image
Step 2: Write the Dockerfile
Create a minimal web server using nginx:alpine with a custom welcome page:
cat > Dockerfile << 'EOF'
FROM nginx:alpine
# Replace the default nginx page with a custom one
RUN echo '<!DOCTYPE html><html><body><h1>CKAD Lab — Custom Image Loaded</h1></body></html>' \
> /usr/share/nginx/html/index.html
EXPOSE 80
EOF
This Dockerfile does three things:
- Uses
nginx:alpineas the base image (small footprint, ~40 MB) - Overwrites the default welcome page with a custom HTML file
- Documents that the container listens on port 80
Why not use latest tag? The Dockerfile specifies nginx:alpine, which is tagged. When you build the image in the next step, you will assign your own tag. Avoiding latest ensures Kubernetes does not try to pull a newer version from a remote registry — which would fail inside Kind.
Step 3: Build the Image
docker build -t ckad-web:v1 ~/ckad-lab/custom-image/
Expected output:
[+] Building 2.1s (6/6) FINISHED
=> [internal] load build definition from Dockerfile
=> [internal] load .dockerignore
=> [internal] load metadata for docker.io/library/nginx:alpine
=> [1/2] FROM docker.io/library/nginx:alpine
=> [2/2] RUN echo '<!DOCTYPE html>...' > /usr/share/nginx/html/index.html
=> exporting to image
=> => naming to docker.io/library/ckad-web:v1
Verify the image exists in your local Docker cache:
docker images ckad-web
Expected output:
REPOSITORY TAG IMAGE ID CREATED SIZE
ckad-web v1 a1b2c3d4e5f6 10 seconds ago 43.2MB
Step 4: Load the Image into Kind
kind load docker-image ckad-web:v1 --name ckad-practice
Expected output:
Image: "ckad-web:v1" with ID "sha256:a1b2c3d4e5f6..." not yet present on node "ckad-practice-control-plane", loading...
Image: "ckad-web:v1" with ID "sha256:a1b2c3d4e5f6..." not yet present on node "ckad-practice-worker", loading...
Image: "ckad-web:v1" with ID "sha256:a1b2c3d4e5f6..." not yet present on node "ckad-practice-worker2", loading...
Kind copies the image from your Docker daemon into each node’s container runtime (containerd). The image is now available on all three nodes.
Verify the image is inside the cluster nodes:
docker exec -it ckad-practice-control-plane crictl images | grep ckad-web
Expected output:
docker.io/library/ckad-web v1 a1b2c3d4e5f6 45.2MB
If ckad-web does not appear in the output, the load command did not succeed. Rerun kind load docker-image ckad-web:v1 --name ckad-practice and check for error messages.
Step 5: Create a Pod Using the Image
Use the imperative command to create the Pod:
k run ckad-web --image=ckad-web:v1 --image-pull-policy=IfNotPresent --port=80
The --image-pull-policy=IfNotPresent flag is critical. Without it, Kubernetes may attempt to pull the image from Docker Hub (the default behavior for non-latest tags varies by cluster configuration). Since ckad-web:v1 does not exist on Docker Hub, the pull would fail with ErrImagePull.
Alternative: Create via YAML manifest. If you prefer the declarative approach (which you should practice for the exam):
k run ckad-web --image=ckad-web:v1 --port=80 $do > ckad-web-pod.yaml
Then edit ckad-web-pod.yaml to add the imagePullPolicy:
apiVersion: v1
kind: Pod
metadata:
labels:
run: ckad-web
name: ckad-web
spec:
containers:
- image: ckad-web:v1
name: ckad-web
ports:
- containerPort: 80
imagePullPolicy: IfNotPresent
restartPolicy: Always
Apply it:
kaf ckad-web-pod.yaml
Step 6: Verify the Pod Is Running
k get pod ckad-web
Expected output:
NAME READY STATUS RESTARTS AGE
ckad-web 1/1 Running 0 15s
The Pod should show 1/1 in the READY column and Running in STATUS. If you see a different status:
| Status | Cause | Fix |
|---|---|---|
ErrImagePull | Image not loaded into Kind | Rerun kind load docker-image ckad-web:v1 --name ckad-practice |
ImagePullBackOff | Same as above — Kubernetes is retrying | Load the image, then delete and recreate the Pod |
CrashLoopBackOff | Container starts and crashes | Check logs: k logs ckad-web |
Pending | No node can schedule the Pod | Check events: k describe pod ckad-web |
For more detail on what happened during scheduling and startup:
k describe pod ckad-web
Look at the Events section at the bottom. A healthy Pod shows events like:
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Scheduled 20s default-scheduler Successfully assigned default/ckad-web to ckad-practice-worker
Normal Pulled 19s kubelet Container image "ckad-web:v1" already present on machine
Normal Created 19s kubelet Created container ckad-web
Normal Started 19s kubelet Started container ckad-web
The message “Container image already present on machine” confirms that the kubelet found the image locally and did not attempt a remote pull.
Step 7: Verify the Web Server Responds
Use kubectl port-forward to create a tunnel from your host to the Pod:
k port-forward pod/ckad-web 8080:80 &
The & runs port-forward in the background so you can continue using the terminal. Traffic to localhost:8080 is now forwarded to port 80 inside the Pod.
Test with curl:
curl http://localhost:8080
Expected output:
<!DOCTYPE html><html><body><h1>CKAD Lab — Custom Image Loaded</h1></body></html>
The custom HTML from your Dockerfile is served by nginx inside the Pod. This confirms the entire pipeline:
- Dockerfile built correctly
- Image loaded into Kind successfully
- Pod created with the correct image and pull policy
- Container started and nginx is serving traffic
- Port forwarding routes traffic from host to Pod
Step 8: Clean Up
Stop the port-forward and delete the Pod:
# Stop background port-forward
kill %1
# Delete the Pod
k delete pod ckad-web $now
The $now variable expands to --force --grace-period 0, which skips the 30-second graceful shutdown period. This is acceptable in a lab environment where you want fast iteration.
Verification Checklist
| # | Verification | Command | Expected |
|---|---|---|---|
| 1 | Image exists in Docker | docker images ckad-web | Shows ckad-web:v1 |
| 2 | Image loaded in Kind | docker exec ckad-practice-worker crictl images | grep ckad-web | Shows ckad-web v1 |
| 3 | Pod is Running | k get pod ckad-web | 1/1 Running |
| 4 | No remote pull attempted | k describe pod ckad-web | grep "already present" | ”already present on machine” |
| 5 | Server responds | curl localhost:8080 | Custom HTML content |
Common Mistakes
Mistake 1: Forgetting imagePullPolicy: IfNotPresent
Without this flag, Kubernetes may try to pull from a remote registry. The default pull policy depends on the tag:
- Tags like
v1,v2,stable: default isIfNotPresent - Tag
latestor no tag: default isAlways
To be safe, always specify imagePullPolicy: IfNotPresent when using locally-loaded images.
Mistake 2: Using the wrong cluster name with kind load
If you have multiple Kind clusters, kind load docker-image loads into the default cluster unless you specify --name. Always include --name ckad-practice (or whatever your cluster is named) to avoid loading into the wrong cluster.
# Check which clusters exist
kind get clusters
Mistake 3: Building with the latest tag
# Wrong — do not do this
docker build -t ckad-web .
This tags the image as ckad-web:latest. When Kubernetes sees a latest tag, it defaults to imagePullPolicy: Always, attempting a remote pull that will fail. Always use explicit version tags: ckad-web:v1, ckad-web:v2, etc.