Hands-On Lab & Practice Questions

6 minute read

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?

Show Answer **Most likely cause:** Missing registry credentials. **Fix:** ```bash # Create the registry secret kubectl create secret docker-registry reg-cred \ --docker-server=registry.internal.io \ --docker-username=user \ --docker-password=pass # Add imagePullSecrets to the Pod spec ``` ```yaml spec: imagePullSecrets: - name: reg-cred containers: - name: app image: registry.internal.io/app:v3 ``` **Other possible causes:** - Wrong image name or tag (image doesn't exist) - Network issues (node can't reach the registry) - `imagePullPolicy: Never` set on the container - Secret exists in a different namespace **Debug with:** ```bash kubectl describe pod | grep -A 10 "Events:" ``` </details> **Q4: Update the image of the `web` container in Deployment `frontend` to `nginx:1.27` using an imperative command.**
Show Answer ```bash kubectl set image deployment/frontend web=nginx:1.27 kubectl rollout status deployment/frontend ``` To verify: ```bash kubectl describe deployment frontend | grep "Image:" # Expected: Image: nginx:1.27 ```
**Q5: What is the default `imagePullPolicy` for an image tagged as `myapp:latest`?**
Show Answer The default `imagePullPolicy` for images tagged `latest` (or with no tag) is **`Always`**. For images with a specific tag (like `myapp:v1.2`), the default is **`IfNotPresent`**. | Image | Default Policy | |-------|---------------| | `myapp` (no tag) | `Always` | | `myapp:latest` | `Always` | | `myapp:v1.2` | `IfNotPresent` | | `myapp@sha256:abc...` | `IfNotPresent` |
**Q6: Your Dockerfile uses `FROM ubuntu:22.04`. Name three ways to improve its security.**
Show Answer 1. **Use a minimal base image** — Switch to `alpine:3.19` or a distroless image to reduce attack surface 2. **Run as non-root** — Add `USER 1000` to the Dockerfile and set `runAsNonRoot: true` in the Pod spec 3. **Pin the exact version** — Use a specific digest: `FROM ubuntu@sha256:abc123...` 4. **Use multi-stage build** — Don't include build tools in the final image 5. **Scan for vulnerabilities** — Run `trivy image` before deployment 6. **Add `.dockerignore`** — Exclude sensitive files like `.env` and `.git`
**Q7: How do you roll back a Deployment to the previous version after a bad image update?**
Show Answer ```bash # Roll back to the immediately previous revision kubectl rollout undo deployment/my-deployment # Or roll back to a specific revision kubectl rollout history deployment/my-deployment kubectl rollout undo deployment/my-deployment --to-revision=3 # Check status kubectl rollout status deployment/my-deployment ```

Updated: