Deploy and Troubleshoot Kubernetes Apps

Deploy, configure, and troubleshoot a Kubernetes app on your local machine.

Introduction

30 Second Summary

When a critical application crashes at 2 AM, someone needs to know how to diagnose and fix it fast. That's exactly what Kubernetes administrators do, and it's what the Certified Kubernetes Administrator (CKA) exam tests.

In this project, you will deploy a web application to a local Kubernetes cluster, configure it with Kubernetes-native primitives, and deliberately break things so you can practice diagnosing and fixing common failures.

What You'll Build

A local Kubernetes cluster running a multi-replica nginx application configured with ConfigMaps, Secrets, and rolling updates.

By the end of this project, you'll have:

  • A running local Kubernetes cluster with a deployed 3-replica application and a ClusterIP Service.
  • Hands-on experience configuring applications with ConfigMaps (volume mounts) and Secrets (environment variables), covering the CKA Workloads domain (15% of the exam).
  • Diagnosed and fixed three common Kubernetes failures: ImagePullBackOff, CrashLoopBackOff, and Pending states, covering the CKA Troubleshooting domain (30% of the exam).
  • Secret Mission: Deploy a multi-container Pod with an init container and sidecar, then debug inter-container communication.

Are there any prerequisites?

No prior Kubernetes experience is needed. This guide includes setup instructions for all required tools (Docker Desktop, kind, and kubectl). You just need a Mac with Homebrew installed.

Install kind and kubectl

To work with Kubernetes you need two things: a cluster to run your workloads and a command-line tool to talk to that cluster.

In this step, you will install kind (which creates a local Kubernetes cluster inside Docker containers) and kubectl (the official Kubernetes command-line tool). Then you will spin up your first cluster and confirm everything is healthy.

In this step, get ready to:
  • Install kind and kubectl on your Mac.
  • Create a local Kubernetes cluster named cka-practice.
  • Explore the control plane components running inside your cluster.
Install kind and kubectl via Homebrew

Homebrew makes it easy to install both tools with a single command each. kind creates lightweight Kubernetes clusters using Docker containers as nodes, while kubectl is what you will use to send commands to that cluster.

  • Install kind by running this command:
brew install kind
  • Install kubectl by running this command:
brew install kubectl

Homebrew will download and install both binaries. This may take a minute depending on your internet speed.

  • Verify both installations by running these commands:
kind version
kubectl version --client

✔️ I see version numbers for both

You should see output similar to kind v0.32.0 and a Client Version block from kubectl. Your exact version numbers may differ, and that is fine.

Both tools are installed and ready to go.

ⓧ Command not found

If your terminal cannot find kind or kubectl, try these fixes:

  • Close and reopen your terminal to reload your PATH.
  • Confirm Homebrew itself works by running brew --version. If that fails, reinstall Homebrew from brew.sh.
  • Re-run the install commands above and watch for error messages in the output.
Create your Kubernetes cluster

With the tools installed, you can now create a cluster. kind will spin up a single-node Kubernetes cluster inside a Docker container on your machine.

  • Create your cluster by running this command:
kind create cluster --name cka-practice

What does this command do?

kind create cluster bootstraps a full Kubernetes cluster inside Docker. The --name cka-practice flag gives your cluster a custom name so you can identify it later.

Behind the scenes, kind uses kubeadm to initialize the cluster. This is the same tool used in the CKA exam environment, so the components you see here will match what you encounter on exam day.

Why kind instead of minikube?

kind runs upstream Kubernetes bootstrapped with kubeadm. This means you see the exact same control plane components (etcd, kube-apiserver, kube-scheduler) that appear on the CKA exam.

Minikube uses VM drivers that can have compatibility issues on Apple Silicon. kind avoids this by running everything as Docker containers.

The cluster creation takes about 30-60 seconds. You will see progress messages as kind pulls the node image and configures the cluster.

  • Verify the cluster is running by checking the cluster info:
kubectl cluster-info --context kind-cka-practice

What is kind-cka-practice?

When kind creates a cluster, it automatically sets your kubectl context to kind-<cluster-name>. Since you named your cluster cka-practice, the context becomes kind-cka-practice. This tells kubectl which cluster to talk to.

  • Confirm the node is ready by running:
kubectl get nodes

You should see one node listed with a STATUS of Ready. This means your cluster is healthy and ready to accept workloads.

Cluster creation failed?

  • Make sure Docker Desktop is running. Open it from your menu bar and confirm you see the green Running status.
  • If you see a message about a cluster already existing, delete it first with kind delete cluster --name cka-practice and then re-run the create command.
  • Check that Docker Desktop has at least 4 GB of RAM allocated in its settings.

Help me troubleshoot my kind cluster creation failure.

Explore the cluster internals

Your cluster is not just one empty box. Kubernetes runs several system components that handle scheduling, networking, storage, and more. These components live in a special namespace called kube-system.

  • View the control plane components by running:
kubectl get pods -n kube-system

What are these pods?

You should see several pods running. Here is what each one does:

  • etcd stores all cluster data (the brain's memory).
  • kube-apiserver is the front door for all cluster commands. Every kubectl command talks to this.
  • kube-controller-manager watches for changes and makes sure the cluster matches the desired state.
  • kube-scheduler decides which node should run each new pod.
  • coredns provides DNS for services inside the cluster so pods can find each other by name.
  • kindnet handles networking between pods on your kind cluster.
  • View the default namespaces in your cluster by running:
kubectl get namespaces

You should see four namespaces: default, kube-system, kube-public, and kube-node-lease. Namespaces are like folders that keep different groups of resources organized.

Why do namespaces matter for the CKA exam?

The CKA exam often asks you to work in specific namespaces. You will see questions like "create a pod in the production namespace" or "find the failing pod in kube-system." Getting comfortable with the -n flag now builds muscle memory for exam day.

Your local Kubernetes cluster is up and running with all its control plane components healthy. Next up, you will deploy your first application into this cluster and expose it so you can access it from your browser.

Deploy Your First Application

Your cluster is up and running with all its control plane components healthy. Now it's time to put that cluster to work by deploying an actual application.

In Kubernetes, you don't run containers directly. Instead, you describe what you want in a YAML manifest, and the cluster figures out how to make it happen. In this step, you'll create two manifests that together run and expose a web server.

In this step, get ready to:
  • Create a Deployment manifest that runs three replicas of nginx.
  • Expose the Deployment with a ClusterIP Service.
  • Access the running application via port-forwarding.
Create a Deployment manifest

A Deployment tells Kubernetes how many copies (replicas) of a container to run and keeps them healthy. If a replica crashes, the Deployment automatically replaces it.

  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create a project folder and move into it by running these commands:
mkdir cka-manifests
cd cka-manifests
  • Confirm the folder exists by running:
ls

You should see cka-manifests listed in the output (it will be empty for now).

  • Create a new file called deployment.yaml with the following content:
# Defines a Deployment that runs 3 replicas of nginx
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-app
  labels:
    app: nginx-app
spec:
  # Number of pod copies to maintain
  replicas: 3
  selector:
    matchLabels:
      app: nginx-app
  template:
    metadata:
      labels:
        app: nginx-app
    spec:
      containers:
      - name: nginx
        image: nginx:1.30.2
        ports:
        - containerPort: 80

What does this manifest do?

This YAML describes the desired state of your application to Kubernetes:

  • replicas: 3 tells the cluster to maintain three identical Pods at all times.
  • selector and labels link the Deployment to its Pods using the key app: nginx-app.
  • image: nginx:1.30.2 pins a specific stable version of nginx, avoiding surprises from uncontrolled updates.
  • containerPort: 80 declares that the container listens on port 80.
  • Save deployment.yaml.

✔️ Awesome, I've got everything!

Great. Make sure you've saved the file before applying it to the cluster.

ⓧ I'd like to double check the full code

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-app
  labels:
    app: nginx-app
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx-app
  template:
    metadata:
      labels:
        app: nginx-app
    spec:
      containers:
      - name: nginx
        image: nginx:1.30.2
        ports:
        - containerPort: 80
  • Apply the manifest to your cluster by running:
kubectl apply -f deployment.yaml

You should see deployment.apps/nginx-app created in the output.

  • Verify the Deployment is running by running:
kubectl get deployments
  • Check that all three Pods are up by running:
kubectl get pods

You should see three pods with names starting with nginx-app- and a STATUS of Running.

Pods not showing Running?

If you see ContainerCreating as the status, wait 10-20 seconds and run kubectl get pods again. The first time a node pulls an image it takes a moment.

If you see an error about the file not being found, make sure you're in the cka-manifests folder where you saved deployment.yaml.

Still stuck? Help me figure out why my kubectl apply is failing.

Expose the Deployment with a Service

Your Pods are running, but they have no stable network address. A Service gives your Pods a single internal IP and distributes traffic across all replicas.

  • Create a new file called service.yaml with the following content:
# Exposes the nginx-app Deployment on a stable internal IP
apiVersion: v1
kind: Service
metadata:
  name: nginx-service
spec:
  # Routes traffic to pods with this label
  selector:
    app: nginx-app
  ports:
  - port: 80
    targetPort: 80
  type: ClusterIP

What does this Service do?

The ClusterIP type creates an internal IP address that only other resources inside the cluster can reach. Traffic sent to port 80 on that IP gets forwarded to port 80 on whichever Pod the load balancer picks.

The selector field matches the app: nginx-app label you set in the Deployment template. This is how the Service knows which Pods belong to it.

  • Save service.yaml.

✔️ Awesome, I've got everything!

Great. Make sure you've saved the file before applying.

ⓧ I'd like to double check the full code

apiVersion: v1
kind: Service
metadata:
  name: nginx-service
spec:
  selector:
    app: nginx-app
  ports:
  - port: 80
    targetPort: 80
  type: ClusterIP
  • Apply the Service manifest by running:
kubectl apply -f service.yaml

You should see service/nginx-service created in the output.

  • Verify the Service is running by running:
kubectl get services

You should see nginx-service listed with type ClusterIP and port 80/TCP.

Don't see nginx-service listed?

Make sure the selector in your service.yaml matches the label in your deployment.yaml exactly: app: nginx-app.

Also confirm you saved the file before running kubectl apply. Unsaved changes won't be picked up.

Need a hand? Help me debug why my Kubernetes Service isn't showing up.

Access the application

Your Service has an internal IP, but it's only reachable from inside the cluster. Port-forwarding lets you tunnel traffic from your laptop into the cluster so you can access the app in your browser.

  • Start port-forwarding to your Service by running:
kubectl port-forward service/nginx-service 8080:80

What does this command do?

This maps port 8080 on your Mac to port 80 on the Service inside the cluster. Any request you send to localhost:8080 gets forwarded to one of the nginx Pods behind the Service.

The terminal will stay busy while port-forwarding is active. You won't be able to type other commands in this terminal until you stop it.

  • Open your browser and visit:
http://localhost:8080

You should see the default nginx welcome page with the heading "Welcome to nginx!" in your browser.

  • Return to your terminal and press Ctrl+C to stop port-forwarding.

Browser showing "connection refused"?

Make sure the kubectl port-forward command is still running in your terminal. If you accidentally closed it or pressed Ctrl+C, run the command again.

Also double check you're visiting http://localhost:8080 (not https, and not a different port number).

Still stuck? Help me troubleshoot why port-forwarding isn't working.

Your application is live and reachable. Next up, you'll customize what nginx serves by injecting configuration with ConfigMaps and Secrets.

Configure with ConfigMaps and Secrets

Your nginx Deployment is live with three replicas behind a Service. Right now, every pod serves the same default nginx welcome page.

In production, you never bake configuration into a container image. Kubernetes provides two primitives for injecting configuration at runtime: ConfigMaps for non-sensitive data and Secrets for credentials.

In this step, get ready to:
  • Create a ConfigMap with custom HTML content.
  • Create a Secret with base64-encoded credentials.
  • Update the Deployment to mount the ConfigMap and inject the Secret.
Create a ConfigMap with custom web content

A ConfigMap stores non-sensitive configuration data as key-value pairs. You'll create one that holds a custom index.html file, which Kubernetes can mount directly into your containers as a file on disk.

  • Create a file called configmap.yaml in the same folder as your other manifests, with the following content:
apiVersion: v1
kind: ConfigMap
metadata:
  name: nginx-content
data:
  index.html: |
    <!DOCTYPE html>
    <html>
    <head><title>CKA Practice</title></head>
    <body>
    <h1>Hello from Kubernetes!</h1>
    <p>This page is served by nginx, configured with a ConfigMap.</p>
    <p>Pod name will vary across replicas - that is load balancing in action.</p>
    </body>
    </html>

What does this ConfigMap do?

The data section contains a key called index.html with your custom HTML page as its value. The pipe character (|) preserves the multi-line formatting of the HTML.

When you mount this ConfigMap as a volume later, Kubernetes creates a file named index.html inside the container at whatever path you specify.

  • Apply the ConfigMap to your cluster by running:
  • Save configmap.yaml first if you haven't already.
kubectl apply -f configmap.yaml
  • You should see configmap/nginx-content created in the output.
  • Confirm it exists by running:
kubectl get configmaps
  • You should see nginx-content listed alongside the default kube-root-ca.crt ConfigMap.

ConfigMap not appearing?

  • Make sure you saved the file before running the apply command.
  • Check that your YAML indentation uses spaces, not tabs. YAML is strict about whitespace.
  • Confirm you're in the correct directory where configmap.yaml lives by running ls to list files.

Why isn't my ConfigMap applying?

✔️ Awesome, I've got everything!

Great. Make sure configmap.yaml is saved and the ConfigMap shows up when you run kubectl get configmaps.

ⓧ I'd like to double check the full code

apiVersion: v1
kind: ConfigMap
metadata:
  name: nginx-content
data:
  index.html: |
    <!DOCTYPE html>
    <html>
    <head><title>CKA Practice</title></head>
    <body>
    <h1>Hello from Kubernetes!</h1>
    <p>This page is served by nginx, configured with a ConfigMap.</p>
    <p>Pod name will vary across replicas - that is load balancing in action.</p>
    </body>
    </html>
Create a Secret

A Kubernetes Secret stores sensitive data like passwords and API keys. Values in a Secret are base64-encoded, which is not encryption but prevents accidental exposure in plain-text logs.

  • Create a file called secret.yaml with the following content:
apiVersion: v1
kind: Secret
metadata:
  name: app-secret
type: Opaque
data:
  DB_PASSWORD: cGFzc3dvcmQxMjM=
  API_KEY: bXktc2VjcmV0LWtleQ==

What do these base64 values mean?

The value cGFzc3dvcmQxMjM= decodes to "password123" and bXktc2VjcmV0LWtleQ== decodes to "my-secret-key". These are demonstration values only.

The type: Opaque means this is a generic Secret. Kubernetes also supports specialized types for TLS certificates and Docker registry credentials.

  • Apply the Secret to your cluster by running:
kubectl apply -f secret.yaml
  • You should see secret/app-secret created in the output.
  • Verify the Secret exists by running:
kubectl get secrets
  • You should see app-secret listed with type Opaque.
  • Inspect the Secret's metadata by running:
kubectl describe secret app-secret
  • The output shows key names (DB_PASSWORD and API_KEY) with their byte sizes, but never the actual values. Kubernetes hides Secret data in describe output.

Secret not created?

  • Verify your base64 values have no trailing spaces. Extra whitespace breaks base64 encoding.
  • Make sure the type: Opaque line is at the same indentation level as metadata and data.

Help me debug my Kubernetes Secret YAML.

✔️ Awesome, I've got everything!

Great. Make sure secret.yaml is saved and kubectl describe secret app-secret shows both keys.

ⓧ I'd like to double check the full code

apiVersion: v1
kind: Secret
metadata:
  name: app-secret
type: Opaque
data:
  DB_PASSWORD: cGFzc3dvcmQxMjM=
  API_KEY: bXktc2VjcmV0LWtleQ==
Update the Deployment to use ConfigMap and Secret

Your ConfigMap and Secret exist in the cluster, but your pods don't know about them yet. You need to update the Deployment to mount the ConfigMap as a volume and inject the Secret as environment variables.

  • Open deployment.yaml and replace its entire contents with this updated manifest:
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-app
  labels:
    app: nginx-app
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx-app
  template:
    metadata:
      labels:
        app: nginx-app
    spec:
      containers:
      - name: nginx
        image: nginx:1.30.2
        ports:
        - containerPort: 80
        envFrom:
        - secretRef:
            name: app-secret
        volumeMounts:
        - name: html-volume
          mountPath: /usr/share/nginx/html
      volumes:
      - name: html-volume
        configMap:
          name: nginx-content

What changed in the Deployment?

Three new sections were added to the container spec:

  • envFrom with secretRef: app-secret injects every key in the Secret as an environment variable inside the container. Your pods now have DB_PASSWORD and API_KEY available.
  • volumeMounts tells the container to mount the volume named html-volume at /usr/share/nginx/html. This is the directory nginx serves files from.
  • volumes at the pod level defines html-volume as sourced from the nginx-content ConfigMap. The ConfigMap's index.html key becomes a file inside the mount path.
  • Save deployment.yaml, then apply the updated manifest by running:
kubectl apply -f deployment.yaml
  • Watch the rolling update complete by running:
kubectl rollout status deployment/nginx-app
  • You should see a message confirming the rollout finished successfully. Kubernetes replaced each old pod with a new one that includes the ConfigMap volume and Secret environment variables.
  • Port-forward to your Service again to see the custom HTML by running:
kubectl port-forward service/nginx-service 8080:80
  • Open http://localhost:8080 in your browser.
  • You should see your custom page with the heading "Hello from Kubernetes!" instead of the default nginx welcome page.
  • Stop the port-forward with Ctrl+C when you're done.

Still seeing the default nginx page?

  • Make sure the rollout completed. Run kubectl get pods and confirm all pods show 1/1 Running with a recent AGE value (they should be newer than your original pods).
  • Verify the ConfigMap name in your Deployment matches exactly. The volume references nginx-content and the ConfigMap metadata name must also be nginx-content.
  • Try a hard refresh in your browser (Cmd+Shift+R on macOS or Ctrl+Shift+R on Windows) to clear cached content.

Why is my ConfigMap content not showing up after port-forward?

✔️ Awesome, I've got everything!

Great. Make sure you see "Hello from Kubernetes!" in your browser and all three pods are running.

ⓧ I'd like to double check the full code

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-app
  labels:
    app: nginx-app
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx-app
  template:
    metadata:
      labels:
        app: nginx-app
    spec:
      containers:
      - name: nginx
        image: nginx:1.30.2
        ports:
        - containerPort: 80
        envFrom:
        - secretRef:
            name: app-secret
        volumeMounts:
        - name: html-volume
          mountPath: /usr/share/nginx/html
      volumes:
      - name: html-volume
        configMap:
          name: nginx-content

Your application is now fully configured through Kubernetes-native primitives. Next up, you'll learn to scale your replicas and perform rolling updates without any downtime.

Scale and Update Your Application

Your application is now fully configured, serving custom HTML through a ConfigMap and injecting credentials through a Secret. The next challenge is managing your application in production, where you need to scale for demand and ship updates without downtime.

Kubernetes handles both scaling and updates declaratively. You tell it the desired state, and it figures out how to get there safely. In this step, you'll scale your replicas, perform a rolling update to swap container images, and recover from a failed deployment using a rollback.

In this step, get ready to:
  • Scale your Deployment up and back down.
  • Perform a rolling update to switch container images.
  • Rollback a bad update and recover your application.
Scale the Deployment

Kubernetes can increase or decrease the number of pods running your application with a single command. This is horizontal scaling, and it's how production clusters handle traffic spikes.

  • Scale your Deployment from 3 replicas to 5 by running this command:
kubectl scale deployment/nginx-app --replicas=5
  • Watch the new pods come up in real time by running:
kubectl get pods -w

What does the -w flag do?

The -w flag stands for "watch." It keeps the terminal open and streams pod status changes as they happen. Press Ctrl+C to stop watching once you see all 5 pods in a Running state.

You should see 5 pods listed, all with a status of Running.

  • Scale back down to 3 replicas by running:
kubectl scale deployment/nginx-app --replicas=3
  • Confirm 2 pods are terminating by running:
kubectl get pods

You should see 3 pods with a status of Running. The terminated pods may briefly show as Terminating before disappearing from the list.

Why is scaling this fast?

Kubernetes doesn't need to rebuild images or reinstall dependencies when scaling. It schedules new pods using the same container image that's already cached on the node. This is one reason containerized applications scale so quickly compared to traditional VMs.

Pods stuck in Pending?

If pods stay in Pending state, your node may not have enough resources. This is unlikely with 5 nginx pods, but can happen if Docker Desktop has limited memory allocated. Check Docker Desktop settings and increase the memory limit if needed.

Ask for help: Why are my pods stuck in Pending state after scaling?

Perform a rolling update

A rolling update replaces pods one at a time so your application stays available during the transition. Kubernetes terminates an old pod only after a new one is ready. This is how you ship changes to production without downtime.

  • Update the nginx image from nginx:1.30.2 to the lighter Alpine variant by running:
kubectl set image deployment/nginx-app nginx=nginx:1.30.2-alpine

What does this command do?

This tells Kubernetes to update the container named nginx inside the nginx-app Deployment to use a different image tag. Kubernetes then starts a rolling update automatically.

The alpine variant is a slimmed-down version of the same nginx image. It's smaller and faster to pull, which is why many production clusters prefer it.

  • Watch the rollout progress by running:
kubectl rollout status deployment/nginx-app

You should see messages about pods being updated, followed by deployment "nginx-app" successfully rolled out.

  • Verify the new image is running by running:
kubectl describe deployment nginx-app | grep Image

You should see Image: nginx:1.30.2-alpine in the output.

Why didn't we edit the YAML file?

The kubectl set image command is an imperative approach. It updates the running cluster directly without modifying your deployment.yaml file. This is fine for quick changes and learning, but in production you would update the YAML file and run kubectl apply to keep your files in sync with the cluster.

Rollout seems stuck?

If kubectl rollout status hangs for more than 60 seconds, the new pods may be failing to start. Press Ctrl+C and run kubectl get pods to check pod statuses. Look for ImagePullBackOff or ErrImagePull which indicate the image tag doesn't exist.

Ask for help: My rolling update is stuck and not completing. How do I diagnose what went wrong?

Rollback a bad update

Not every deployment goes smoothly. When you push a bad image or broken configuration, Kubernetes keeps the previous working state in its rollout history. You can undo the damage with a single command.

  • Simulate a bad deployment by setting an image tag that doesn't exist:
kubectl set image deployment/nginx-app nginx=nginx:nonexistent-tag
  • Watch the failure unfold by running:
kubectl get pods

You should see some pods stuck in ImagePullBackOff or ErrImagePull status. Kubernetes can't find the tag nonexistent-tag on Docker Hub, so the new pods fail to start.

What is ImagePullBackOff?

This status means Kubernetes tried to pull the container image and failed. The "BackOff" part means it's retrying with increasing delays between attempts. This is one of the most common pod failures you'll encounter in production and on the CKA exam.

  • Roll back to the previous working image by running:
kubectl rollout undo deployment/nginx-app

What does rollout undo do?

Kubernetes stores a history of your deployment revisions. The undo command reverts your Deployment to the previous revision, which in this case is the working nginx:1.30.2-alpine image. Your application recovers automatically without manual intervention.

  • Confirm all pods are healthy again by running:
kubectl get pods

You should see 3 pods all in Running status. Your application has recovered.

  • Verify the image is back to the working version by running:
kubectl describe deployment nginx-app | grep Image

You should see Image: nginx:1.30.2-alpine confirmed in the output.

Still seeing failed pods after rollback?

If some pods still show ImagePullBackOff after the undo, wait 30 seconds and run kubectl get pods again. Kubernetes needs a moment to terminate the failed pods and schedule new ones. If the issue persists, run kubectl rollout status deployment/nginx-app to confirm the rollback completed.

Ask for help: My pods are still failing after running kubectl rollout undo. How do I fix this?

You've now scaled, updated, and recovered a live Deployment using only kubectl commands. Next up, you'll intentionally break pods in three different ways and practice diagnosing them from scratch.

Troubleshoot Broken Deployments

In the previous step, you scaled your Deployment up and down, performed a rolling update, and recovered from a bad image tag with a rollback.

Troubleshooting is the single largest domain on the CKA exam, worth 30% of your score. In this step, you will intentionally break three pods in different ways, then diagnose and fix each one using the same kubectl commands you would use on the real exam.

In this step, get ready to:
  • Diagnose and fix an ImagePullBackOff error.
  • Diagnose and fix a CrashLoopBackOff error.
  • Diagnose and fix a Pending pod caused by impossible resource requests.
Diagnose ImagePullBackOff

When Kubernetes cannot pull a container image, the pod enters an ImagePullBackOff state. This happens when the image tag does not exist, the registry is unreachable, or authentication fails.

  • Create a file called broken-pod-1.yaml in the same folder as your other manifests, with the following content:
apiVersion: v1
kind: Pod
metadata:
  name: broken-pod-1
  labels:
    exercise: troubleshooting
spec:
  containers:
  - name: app
    # Intentionally invalid image tag to trigger ImagePullBackOff
    image: nginx:nonexistent-tag
    ports:
    - containerPort: 80

What does this manifest do?

This defines a single Pod named broken-pod-1 that tries to pull nginx:nonexistent-tag. That tag does not exist on Docker Hub, so Kubernetes will fail to pull the image.

  • Apply the broken manifest by running:
kubectl apply -f broken-pod-1.yaml
  • Check the pod status by running:
kubectl get pods broken-pod-1

After a few seconds, you should see the STATUS column show ErrImagePull or ImagePullBackOff.

  • Dig into the error details by running:
kubectl describe pod broken-pod-1

What should I look for?

Scroll down to the Events section at the bottom of the output. You will see Warning events with reason Failed indicating that the kubelet could not pull the image.

The message will mention the image name nginx:nonexistent-tag and a description like "manifest unknown". This tells you the image tag does not exist on the registry.

Now that you have identified the problem, fix the pod.

  • Delete the broken pod by running:
kubectl delete pod broken-pod-1
  • In broken-pod-1.yaml, find this line:
    image: nginx:nonexistent-tag
  • Replace nginx:nonexistent-tag with nginx:1.30.2, so the line looks like this:
    image: nginx:1.30.2
  • Reapply the fixed manifest by running:
kubectl apply -f broken-pod-1.yaml
  • Confirm the pod is now running:
kubectl get pods broken-pod-1

The STATUS column should now show Running.

Still seeing ImagePullBackOff?

Make sure you saved broken-pod-1.yaml after editing. Also confirm you deleted the original pod before reapplying. Kubernetes pods are immutable once created, so you cannot just reapply over an existing pod.

help me fix my pod that is still in ImagePullBackOff after I edited the YAML

Diagnose CrashLoopBackOff

A CrashLoopBackOff means the container starts, exits immediately, and Kubernetes keeps restarting it with increasing back-off delays. This typically happens when the container's command fails or the application crashes on startup.

  • Create a file called broken-pod-2.yaml with the following content:
apiVersion: v1
kind: Pod
metadata:
  name: broken-pod-2
  labels:
    exercise: troubleshooting
spec:
  containers:
  - name: app
    image: nginx:1.30.2
    # This command exits immediately with a failure code
    command: ["/bin/sh", "-c", "exit 1"]
    ports:
    - containerPort: 80

What does this manifest do?

This pod uses a valid image (nginx:1.30.2) but overrides the default command with exit 1. The container starts, runs that shell command, and immediately terminates with a non-zero exit code. Kubernetes sees the failure and restarts the container, creating the CrashLoopBackOff cycle.

  • Apply the broken manifest by running:
kubectl apply -f broken-pod-2.yaml
  • Wait about 30 seconds, then check the pod status:
kubectl get pods broken-pod-2

You should see CrashLoopBackOff in the STATUS column, with the RESTARTS count increasing.

  • Check the container logs by running:
kubectl logs broken-pod-2

Why are the logs empty?

The exit 1 command produces no output before terminating. When logs are empty, your next move is kubectl describe to check the Events and container state.

  • Get more detail by running:
kubectl describe pod broken-pod-2

In the output, look for the Last State field under the container status. It shows Terminated with Exit Code: 1, confirming the command is the problem.

  • Delete the broken pod by running:
kubectl delete pod broken-pod-2
  • In broken-pod-2.yaml, find this line:
    command: ["/bin/sh", "-c", "exit 1"]
  • Remove the entire command line so the container uses nginx's default startup command instead.
  • Reapply the fixed manifest by running:
kubectl apply -f broken-pod-2.yaml
  • Confirm the pod is running:
kubectl get pods broken-pod-2

The STATUS should now show Running with 0 restarts.

Pod still in CrashLoopBackOff?

Double-check that you deleted the entire command: line from the YAML, not just the value. Also make sure you ran kubectl delete pod broken-pod-2 before reapplying.

help me fix my CrashLoopBackOff pod

Diagnose a Pending pod

A Pending pod means the Kubernetes scheduler cannot find a node with enough resources to run it. On the CKA exam, you will see this when a pod requests more CPU or memory than any node in the cluster can provide.

  • Create a file called broken-pod-3.yaml with the following content:
apiVersion: v1
kind: Pod
metadata:
  name: broken-pod-3
  labels:
    exercise: troubleshooting
spec:
  containers:
  - name: app
    image: nginx:1.30.2
    ports:
    - containerPort: 80
    resources:
      requests:
        # 100Gi is far more memory than any single node has
        memory: "100Gi"

What does this manifest do?

This pod requests 100Gi of memory. Your kind cluster node only has a few gigabytes available. The scheduler cannot place the pod on any node, so it stays Pending indefinitely.

  • Apply the manifest by running:
kubectl apply -f broken-pod-3.yaml
  • Check the pod status by running:
kubectl get pods broken-pod-3

The STATUS column shows Pending. Unlike the previous errors, this pod never even starts. It is stuck waiting for a node.

  • Investigate why the pod is stuck by running:
kubectl describe pod broken-pod-3

What should I look for?

In the Events section, look for a Warning event with reason FailedScheduling. The message will say something like "0/1 nodes are available: 1 Insufficient memory."

This tells you the scheduler examined all nodes in your cluster and none had enough free memory to satisfy the 100Gi request.

  • Delete the broken pod by running:
kubectl delete pod broken-pod-3
  • In broken-pod-3.yaml, find this line:
        memory: "100Gi"
  • Replace it with a reasonable value:
        memory: "128Mi"
  • Reapply the fixed manifest by running:
kubectl apply -f broken-pod-3.yaml
  • Confirm all three exercise pods are now running:
kubectl get pods -l exercise=troubleshooting

All three pods (broken-pod-1, broken-pod-2, broken-pod-3) should show Running in the STATUS column.

Pod still Pending after fixing memory?

Make sure you deleted the old pod before reapplying. Also confirm the indentation in your YAML is correct. The memory field must be nested under requests, which is nested under resources.

help me fix my Pending pod

The troubleshooting pattern for the CKA exam

Every troubleshooting scenario on the CKA exam follows the same workflow you just practiced:

  • kubectl get pods to identify the symptom (ImagePullBackOff, CrashLoopBackOff, Pending).
  • kubectl describe pod <name> to read the Events section for the root cause.
  • kubectl logs <name> to check application output when the container ran.
  • Fix the manifest and reapply.

You have just practiced the exact troubleshooting workflow that accounts for 30% of the CKA exam. Combined with the workload management skills from earlier steps, you have covered 45% of the exam domains in a single project.

Secret mission

Deploy a Multi-Container Pod with Init Container and Sidecar

Real-world Kubernetes Pods rarely contain a single container. In this mission, you will build a Pod with three containers that communicate through a shared volume. An init container prepares data before the main containers start, and a sidecar continuously writes log entries alongside the primary web server.

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs entirely locally, so there are no ongoing financial costs. The kind cluster uses approximately 500MB-1GB of RAM while running.

Resources you used:

  • kind cluster cka-practice (running as Docker containers on your machine).
  • YAML manifest files (deployment.yaml, service.yaml, configmap.yaml, secret.yaml, broken-pod-1.yaml, broken-pod-2.yaml, broken-pod-3.yaml, multi-container-pod.yaml).
  • kind and kubectl (installed via Homebrew).

Keep everything running

No action needed. Choose this if you plan to continue practicing Kubernetes commands or want to explore the cluster further.

  • The cluster persists until you delete it or restart Docker Desktop.
  • It uses approximately 500MB-1GB of RAM while running.
  • Your YAML files remain on disk for reference.
  • kind and kubectl remain installed for future projects.

Pause - I'll come back to this later

Stop Docker Desktop to free up memory. The cluster will restart when you relaunch Docker Desktop.

  • Click the Docker Desktop icon in your menu bar.
  • Select Quit Docker Desktop.
  • When you are ready to resume, relaunch Docker Desktop. Your cluster and all resources will come back automatically.

Delete - I don't want to use this again

Remove all project resources and start fresh. This deletes the cluster, your local files, and the CLI tools.

Delete the kind cluster by running:

kind delete cluster --name cka-practice

You should see a confirmation message that the cluster was deleted.

Remove the YAML manifest files by running:

rm deployment.yaml service.yaml configmap.yaml secret.yaml broken-pod-1.yaml broken-pod-2.yaml broken-pod-3.yaml multi-container-pod.yaml

Uninstall kind and kubectl by running:

brew uninstall kind
brew uninstall kubectl

What about Docker Desktop?

Docker Desktop was already on your machine before this project. You only need to remove it if you no longer use it for other work.

Nice Work!

Nice Work!

You just built a local Kubernetes cluster from scratch and deployed, configured, scaled, and troubleshot a real application entirely from the command line. That covers 45% of the CKA exam weight across the Troubleshooting and Workloads domains.

You've learned how to:

  • Deploy a multi-replica nginx application with a ClusterIP Service, and configure it using ConfigMaps (volume mounts) and Secrets (environment variables).
  • Perform rolling updates and rollbacks on a live Deployment, scaling replicas up and down without downtime.
  • Diagnose and fix three common Kubernetes failures: ImagePullBackOff, CrashLoopBackOff, and Pending using kubectl describe and kubectl logs.
  • Secret Mission: Built a multi-container Pod with an init container and sidecar, and debugged inter-container communication using shared volumes.

Ready to quiz yourself?