Hands-On Lab & Practice Questions
Hands-On Labs
These labs reinforce the container image concepts covered in this chapter. Complete them in order to build practical skills.
Lab 1: Build and Deploy a Custom Image
Scenario: Create a simple Nginx-based image with a custom homepage and deploy it to Kubernetes.
Steps:
# Create a project directory
mkdir custom-nginx && cd custom-nginx
# Create the custom HTML page
cat <<EOF > index.html
<html>
<body>
<h1>Hello from CKAD!</h1>
<p>This page is served from a custom container image.</p>
</body>
</html>
EOF
# Create the Dockerfile
cat <<EOF > Dockerfile
FROM nginx:1.27-alpine
COPY index.html /usr/share/nginx/html/index.html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
EOF
# Build the image
docker build -t custom-nginx:v1 .
# Test locally
docker run -d -p 8080:80 --name test-nginx custom-nginx:v1
curl localhost:8080
# Expected: <html><body><h1>Hello from CKAD!</h1>...</body></html>
# Clean up local container
docker stop test-nginx && docker rm test-nginx
Deploy to Kubernetes:
# custom-nginx-pod.yaml
apiVersion: v1
kind: Pod
metadata:
name: custom-nginx
labels:
app: custom-nginx
spec:
containers:
- name: nginx
image: custom-nginx:v1
imagePullPolicy: Never # Use local image
ports:
- containerPort: 80
kubectl apply -f custom-nginx-pod.yaml
kubectl get pod custom-nginx
kubectl port-forward pod/custom-nginx 8080:80 &
curl localhost:8080
Verification:
kubectl describe pod custom-nginx | grep "Image:"
# Expected: Image: custom-nginx:v1
Lab 2: CMD and ENTRYPOINT Override in Kubernetes
Scenario: Understand how Kubernetes command and args override Dockerfile ENTRYPOINT and CMD.
Step 1: Create a base image with ENTRYPOINT and CMD
# greeter/Dockerfile
FROM busybox:1.36
ENTRYPOINT ["echo"]
CMD ["Hello from Docker!"]
mkdir greeter && cd greeter
# Create the Dockerfile above
docker build -t greeter:v1 .
# Test defaults
docker run greeter:v1
# Output: Hello from Docker!
# Override CMD
docker run greeter:v1 "Hello from CLI!"
# Output: Hello from CLI!
Step 2: Override in Kubernetes
# lab2-default.yaml — uses original ENTRYPOINT + CMD
apiVersion: v1
kind: Pod
metadata:
name: greeter-default
spec:
containers:
- name: greeter
image: greeter:v1
imagePullPolicy: Never
restartPolicy: Never
# lab2-args.yaml — override CMD only
apiVersion: v1
kind: Pod
metadata:
name: greeter-args
spec:
containers:
- name: greeter
image: greeter:v1
imagePullPolicy: Never
args: ["Hello from Kubernetes args!"]
restartPolicy: Never
# lab2-command.yaml — override ENTRYPOINT and CMD
apiVersion: v1
kind: Pod
metadata:
name: greeter-command
spec:
containers:
- name: greeter
image: greeter:v1
imagePullPolicy: Never
command: ["/bin/sh", "-c"]
args: ["echo 'Completely overridden!' && date"]
restartPolicy: Never
Run all three and compare:
kubectl apply -f lab2-default.yaml
kubectl apply -f lab2-args.yaml
kubectl apply -f lab2-command.yaml
# Wait for completion
kubectl wait --for=condition=Ready=false pod/greeter-default --timeout=30s 2>/dev/null || true
sleep 5
# Check logs
kubectl logs greeter-default
# Output: Hello from Docker!
kubectl logs greeter-args
# Output: Hello from Kubernetes args!
kubectl logs greeter-command
# Output: Completely overridden!
# Mon Feb 9 10:00:00 UTC 2025
Mapping summary:
| Dockerfile | K8s Pod Spec | Lab Result |
|---|---|---|
ENTRYPOINT ["echo"] + CMD ["Hello from Docker!"] |
(nothing) | Hello from Docker! |
ENTRYPOINT ["echo"] |
args: ["Hello from Kubernetes args!"] |
Hello from Kubernetes args! |
| (overridden) | command: ["/bin/sh", "-c"] + args: [...] |
Completely overridden! |
Lab 3: Multi-Stage Build and Deploy
Scenario: Build a Go application using a multi-stage Dockerfile and deploy it.
mkdir go-app && cd go-app
# Create a simple Go application
cat <<'EOF' > main.go
package main
import (
"fmt"
"net/http"
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello from Go multi-stage build!\n")
})
fmt.Println("Server starting on :8080")
http.ListenAndServe(":8080", nil)
}
EOF
cat <<EOF > go.mod
module go-app
go 1.22
EOF
# Create multi-stage Dockerfile
cat <<'EOF' > Dockerfile
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.mod .
COPY main.go .
RUN CGO_ENABLED=0 go build -o /server .
FROM alpine:3.19
RUN adduser -D -u 1000 appuser
COPY --from=builder /server /server
USER 1000
EXPOSE 8080
CMD ["/server"]
EOF
# Build
docker build -t go-app:v1 .
# Check image size
docker images go-app:v1
# Notice: the final image is ~15MB instead of 300MB+
Deploy to Kubernetes:
# go-app-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: go-app
spec:
replicas: 2
selector:
matchLabels:
app: go-app
template:
metadata:
labels:
app: go-app
spec:
containers:
- name: go-app
image: go-app:v1
imagePullPolicy: Never
ports:
- containerPort: 8080
securityContext:
runAsNonRoot: true
allowPrivilegeEscalation: false
kubectl apply -f go-app-deployment.yaml
kubectl rollout status deployment/go-app
kubectl port-forward deployment/go-app 8080:8080 &
curl localhost:8080
# Output: Hello from Go multi-stage build!
Lab 4: Private Registry Simulation
Scenario: Create a registry secret and use it in a Pod.
# Create a docker-registry secret (simulated credentials)
kubectl create secret docker-registry my-reg-secret \
--docker-server=registry.example.com \
--docker-username=testuser \
--docker-password=testpass123
# Verify the secret
kubectl get secret my-reg-secret -o jsonpath='{.data.\.dockerconfigjson}' | base64 -d | python3 -m json.tool
# private-app.yaml
apiVersion: v1
kind: Pod
metadata:
name: private-app
spec:
containers:
- name: app
image: registry.example.com/team/my-app:v1
imagePullSecrets:
- name: my-reg-secret
kubectl apply -f private-app.yaml
# This will fail (registry doesn't exist), but check the events to see
# that Kubernetes attempted to use the secret:
kubectl describe pod private-app | grep -A 5 "Events:"
# You should see "pulling image" with the registry URL — meaning the secret was used
Practice Questions
Q1: What is the difference between CMD and ENTRYPOINT in a Dockerfile?
Show Answer
**CMD** sets the default command that can be overridden when the container starts. **ENTRYPOINT** sets a fixed command — any additional arguments are appended to it. In Kubernetes: - `command` overrides ENTRYPOINT - `args` overrides CMD Example: ```yaml containers: - name: app image: myimage command: ["echo"] # Overrides ENTRYPOINT args: ["hello world"] # Overrides CMD ```Q2: Write a multi-stage Dockerfile for a Go application that produces a minimal final image.
Show Answer
```dockerfile FROM golang:1.22 AS builder WORKDIR /app COPY . . RUN CGO_ENABLED=0 go build -o /app/server . FROM alpine:3.19 COPY --from=builder /app/server /server USER 1000 EXPOSE 8080 CMD ["/server"] ``` The builder stage compiles the Go binary. The final stage uses Alpine with just the binary — resulting in an image under 20MB. For an even smaller image, use `scratch` instead of `alpine`: ```dockerfile FROM scratch COPY --from=builder /app/server /server CMD ["/server"] ```Q3: A Pod cannot pull the image registry.internal.io/app:v3. What could be wrong and how do you fix it?