Deploy and Manage an App on Kubernetes
Deploy, scale, and recover a containerized app on a local Kubernetes cluster.
Introduction
30 Second Summary
Every time you deploy an app update, something could go wrong. A typo in a config file, a bad image tag, or a dependency that vanished overnight. The real question is not whether failures happen, but how fast you can recover.
In this project, you will deploy a web server to a local Kubernetes cluster, scale it to handle more traffic, intentionally push a broken update, and recover in seconds using rollback.
What You'll Build
You will open your browser, hit localhost:30080, and see Kubernetes serving your requests across multiple replicas that you deployed, scaled, broke, and recovered yourself.
By the end of this project, you'll have:
- A running Deployment with multiple replicas you can scale up and down at will.
- A Service that routes browser traffic to your pods, accessible at localhost:30080.
- A rollback recovery from a deliberately broken update, proving Kubernetes protects your running app from bad deployments.
- Secret Mission: Add liveness and readiness probes so Kubernetes auto-detects unhealthy pods.
Are there any prerequisites?
You need Docker Desktop installed on macOS with its built-in Kubernetes feature enabled. If you have not done that yet, enable it in Docker Desktop's Settings > Kubernetes > Enable Kubernetes before starting.
Before We Start
Before we dive in, let's lock in what you're about to build and why it matters.
Verify Your Cluster and Set Up Your Project
Every command in this project talks to your local Kubernetes cluster through kubectl. If the cluster is not running or kubectl cannot reach it, nothing else works.
In this step, you will confirm your Docker Desktop Kubernetes cluster is healthy and responding. Then you will create a project folder where your YAML manifests will live.
In this step, get ready to:
- Verify kubectl is connected to your Docker Desktop Kubernetes cluster.
- Create a project directory and open it in VS Code.
Verify kubectl is connected to your cluster
Docker Desktop installs kubectl automatically when you enable Kubernetes. Your first job is confirming it can talk to the cluster.
- Press Cmd+Space to open Spotlight search.
- Type Terminal and press Enter to open it.
- Point kubectl at your Docker Desktop cluster by running this command:
kubectl config use-context docker-desktop
You should see the message: Switched to context "docker-desktop".
What does this command do?
Your machine could have multiple Kubernetes clusters configured (Docker Desktop, minikube, a cloud cluster). This command tells kubectl which cluster to talk to for all following commands.
Seeing 'error: no context exists'?
This means Kubernetes is not enabled in Docker Desktop. Open Docker Desktop, go to Settings then Kubernetes, and check Enable Kubernetes. Click Apply & Restart and wait for the Kubernetes status indicator to turn green.
help me enable Kubernetes in Docker Desktop
Before you run the next command, what do you expect to see if your cluster is healthy?
- Confirm the cluster node is running by running this command:
kubectl get nodes
You should see a table with a node named docker-desktop and STATUS Ready.
✔️ I see docker-desktop with Ready status
Your cluster is healthy and ready for deployments. Move on to the next command.
ⓧ I see NotReady status
Kubernetes is still starting up. Wait 30 seconds and run kubectl get nodes again. If it stays NotReady for over two minutes, restart Docker Desktop by clicking the Docker icon in your menu bar and selecting Restart.
ⓧ Command not found
Docker Desktop may not have installed kubectl to your PATH. Open Docker Desktop, go to Settings then Kubernetes, and confirm Enable Kubernetes is checked. Then quit and reopen your Terminal.
help me get kubectl working on my Mac with Docker Desktop
- Confirm the client and server are communicating by running:
kubectl version
You should see both a Client Version and a Server Version in the output. The Server Version confirms the cluster is responding to API calls.
What do these versions mean?
The Client Version is your local kubectl binary. The Server Version is the Kubernetes API server running inside Docker Desktop. Both printing means your toolchain is fully connected.
Create your project directory
You need a dedicated folder to hold the YAML manifest files you will write in the next steps.
- Create a project folder and move into it by running these commands:
mkdir ~/k8s-first-deploy && cd ~/k8s-first-deploy
- Confirm you are inside the new folder by running:
pwd
You should see /Users/<your-username>/k8s-first-deploy printed in the terminal.
- Open this folder in VS Code by running:
code .
VS Code opens with an empty k8s-first-deploy folder in the Explorer sidebar.
Seeing 'command not found: code'?
The code command needs to be installed in your PATH. Open VS Code manually (Cmd+Space, type Visual Studio Code, press Enter). Then press Cmd+Shift+P and type Shell Command: Install 'code' command in PATH. Select it and try code . again in your terminal.
help me install the code command on macOS
Your cluster is verified and your workspace is ready. Next up, you will write your first Kubernetes manifests and deploy a live web server.
Deploy and Expose Your App
Your cluster is healthy and your project directory is ready. Now you need something actually running on it.
In this step, you will write two YAML manifests that tell Kubernetes what container to run and how to expose it to your browser. By the end, you will have a live nginx web server you can visit at localhost:30080.
In this step, get ready to:
- Write and apply a Deployment manifest that runs nginx pods on your cluster.
- Create a Service that routes browser traffic to your pods on port 30080.
- Visit your live app in the browser and confirm it is serving traffic.
Create your Deployment manifest
A Deployment tells Kubernetes which container image to run and how many copies (replicas) to keep alive. If a pod crashes, the Deployment automatically replaces it.
- In VS Code, create a new file called deployment.yaml in your k8s-first-deploy folder.
- Paste the following manifest into the file:
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-app
labels:
app: nginx-app
spec:
replicas: 2
selector:
matchLabels:
app: nginx-app
template:
metadata:
labels:
app: nginx-app
spec:
containers:
- name: nginx
image: nginx:1.14.2
ports:
- containerPort: 80
What does this manifest do?
- apiVersion: apps/v1 tells Kubernetes which API group handles Deployments.
- replicas: 2 means Kubernetes will run two copies of your container as separate pods.
- selector.matchLabels and template.metadata.labels must match so the Deployment knows which pods belong to it.
- image: nginx:1.14.2 pulls the official nginx web server image from Docker Hub.
- containerPort: 80 declares the port nginx listens on inside the container.
✔️ Awesome, I've got everything!
Great. Double check you have saved your file before moving on.
ⓧ I'd like to double check the full code
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-app
labels:
app: nginx-app
spec:
replicas: 2
selector:
matchLabels:
app: nginx-app
template:
metadata:
labels:
app: nginx-app
spec:
containers:
- name: nginx
image: nginx:1.14.2
ports:
- containerPort: 80
- Save deployment.yaml.
- Apply the manifest to your cluster by running this command:
kubectl apply -f deployment.yaml
You should see deployment.apps/nginx-app created in the output.
- Verify your pods are running by running this command:
kubectl get pods
You should see 2 pods with names starting with nginx-app and STATUS Running.
Pods not showing as Running?
- If STATUS shows ContainerCreating, wait a few seconds and run kubectl get pods again. Kubernetes is pulling the nginx image for the first time.
- If you see an error about indentation, open deployment.yaml and check that every level is indented with exactly 2 spaces. YAML is whitespace-sensitive.
- If pods show ImagePullBackOff, confirm the image line reads exactly nginx:1.14.2 with no typos.
Still stuck? Help me debug why my Kubernetes pods are not running after applying deployment.yaml.
Expose your app with a Service
Your pods are running, but they are isolated inside the cluster. A Service creates a stable network endpoint that routes traffic from your machine into those pods.
- In VS Code, create a new file called service.yaml in your k8s-first-deploy folder.
- Paste the following manifest into the file:
apiVersion: v1
kind: Service
metadata:
name: nginx-service
labels:
app: nginx-app
spec:
type: NodePort
selector:
app: nginx-app
ports:
- protocol: TCP
port: 80
targetPort: 80
nodePort: 30080
What does this manifest do?
- type: NodePort exposes the Service on a static port on your machine.
- selector: app: nginx-app tells the Service to route traffic to pods with the matching label.
- port: 80 is the Service's own port inside the cluster.
- targetPort: 80 is the port on the container that receives the traffic.
- nodePort: 30080 is the port you will use in your browser to reach the app.
✔️ Awesome, I've got everything!
Great. Double check you have saved your file before moving on.
ⓧ I'd like to double check the full code
apiVersion: v1
kind: Service
metadata:
name: nginx-service
labels:
app: nginx-app
spec:
type: NodePort
selector:
app: nginx-app
ports:
- protocol: TCP
port: 80
targetPort: 80
nodePort: 30080
- Save service.yaml.
- Apply the manifest by running this command:
kubectl apply -f service.yaml
You should see service/nginx-service created in the output.
- Verify the Service exists by running this command:
kubectl get services
You should see nginx-service listed with TYPE NodePort and PORT(S) showing 80:30080/TCP.
Service not showing up?
- Check that selector in service.yaml reads app: nginx-app. This must match the label on your pods exactly.
- If you see an error about nodePort, confirm the value is 30080. Kubernetes only allows NodePort values in the range 30000-32767.
- If the apply command shows a YAML parsing error, check indentation. The ports: block must be indented under spec:.
Still stuck? Help me debug why my Kubernetes Service is not appearing after applying service.yaml.
Visit your running app
Both your Deployment and NodePort Service are live. Traffic hitting port 30080 on your machine now routes through the Service to one of your nginx pods.
Before you open the browser, what do you think you will see at localhost:30080?
- Open your browser and navigate to http://localhost:30080.
You should see the "Welcome to nginx!" page. This page is being served by the pods you deployed, routed through the Service you created.
Not seeing the nginx page?
- Run kubectl get pods and confirm all pods show STATUS Running. If pods are not ready, the Service has nothing to route to.
- Run kubectl get services and confirm nginx-service shows PORT(S) 80:30080/TCP. If the port is different, re-check your service.yaml.
- Make sure Docker Desktop is running. If Docker Desktop is stopped, your Kubernetes cluster is offline.
Still stuck? Help me figure out why I cannot reach my nginx app at localhost:30080.
Your app is live and serving traffic through Kubernetes. Next up, you will scale it to handle more load by adding replicas.
Scale Your Deployment
Your app is live and serving traffic from 2 replicas. With only 2 pods, your app has limited resilience. If both fail at the same time, your users get nothing.
Scaling to more replicas is how Kubernetes keeps apps highly available. It is also a prerequisite for zero-downtime updates in the next step.
In this step, get ready to:
- Scale your deployment from 2 to 4 replicas.
- Watch Kubernetes create new pods in real time.
- Confirm each pod gets its own IP address for load-balancing.
Scale your replicas from 2 to 4
A Deployment declares how many identical pods Kubernetes should keep running. Changing the replicas field and re-applying the file is all it takes to scale up.
- In VS Code, open deployment.yaml and find the replicas field under spec:.
- Find this line:
replicas: 2
- Replace 2 with 4, so the line now looks like this:
replicas: 4
- Save deployment.yaml.
✔️ Awesome, I've got everything!
Great. Double check you have saved the file, then continue below.
ⓧ I'd like to double check the full code
Here is the complete deployment.yaml at this point:
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-app
labels:
app: nginx-app
spec:
replicas: 4
selector:
matchLabels:
app: nginx-app
template:
metadata:
labels:
app: nginx-app
spec:
containers:
- name: nginx
image: nginx:1.14.2
ports:
- containerPort: 80
- Apply the updated manifest by running this command:
kubectl apply -f deployment.yaml
You should see deployment.apps/nginx-app configured in the output. Kubernetes now knows you want 4 replicas instead of 2.
- Verify the pods are running by running:
kubectl get pods
You should see 4 pods listed, all with STATUS Running.
Seeing fewer than 4 pods?
Make sure you saved the file after changing the replicas value. Re-run the apply command after saving.
If pods show Pending status, wait 10-15 seconds and run the command again. Docker Desktop may need a moment to schedule them.
Why are my pods not scaling up?
Watch Kubernetes create new pods in real time
The -w flag on kubectl get pods opens a live stream. You can see each pod's status change as Kubernetes schedules it.
- Watch the pods in real time by running:
kubectl get pods -w
You should see all 4 pods listed with STATUS Running. New lines appear as Kubernetes updates each pod's state.
- Press Ctrl+C to stop watching once you see all 4 pods are Running.
What does the -w flag do?
The -w flag stands for "watch." It keeps the command running and prints a new line every time a pod's status changes. This is useful for observing rollouts, scaling events, and failures as they happen.
Confirm load-balancing across pods
Each pod in a Deployment gets its own unique IP address. Kubernetes distributes incoming traffic across all of them. The -o wide flag shows these IPs.
Before you run the next command, do you think each pod will have its own unique IP address, or will they all share one?
- Check the pod IPs by running:
kubectl get pods -o wide
Each pod has its own IP in the output. Kubernetes is now load-balancing traffic across all 4 replicas through your Service.
Why does each pod need its own IP?
Your Service acts as a single entry point (port 30080). Behind the scenes, it distributes requests to individual pod IPs. If one pod goes down, the Service routes traffic to the remaining healthy pods automatically.
- Open http://localhost:30080 in your browser to confirm the app is still serving traffic across your scaled replicas.
You should still see the nginx welcome page. Your app is now running on 4 pods instead of 2, with Kubernetes handling the distribution.
Your deployment is now scaled to 4 replicas, giving your app real resilience. Next up, you will intentionally break your deployment with a bad update to see how Kubernetes protects your running pods from failure.
Push a Bad Update
Your deployment is running smoothly with 4 replicas, load-balancing traffic across all of them. But what happens when you deploy a broken version of your app?
In production, bad deployments happen all the time. A typo in an image tag, a registry outage, a missing build artifact. In this step, you will intentionally push a broken update and observe how Kubernetes responds.
In this step, get ready to:
- Push a container image update that references a tag that does not exist.
- Watch Kubernetes detect the failure and stall the rollout.
- Diagnose the failure and observe which pods survived.
Push a broken image update
You are going to tell Kubernetes to update your pods to an image version that does not exist on Docker Hub. This simulates a common production mistake: referencing a container image tag that was never built or pushed.
Before you run this, think about what you expect to happen. You have 4 pods running right now. If Kubernetes tries to update them to an image that does not exist, do you think it will kill all 4 pods at once? Or will some survive?
- Push the broken update by running this command:
kubectl set image deployment/nginx-app nginx=nginx:9.99.99
You should see deployment.apps/nginx-app image updated in your terminal. Kubernetes accepted your request and is now attempting the update.
What does this command do?
kubectl set image tells Kubernetes to change the container image for a running deployment without editing the YAML file directly.
The tag nginx:9.99.99 does not exist on Docker Hub. Kubernetes will try to pull it, fail, and you will get to see exactly how it handles that failure.
Watch the rollout stall
- Check the rollout progress by running this command:
kubectl rollout status deployment/nginx-app
This command hangs. It is waiting for the rollout to complete, but the new pods cannot start because the image does not exist. You will see a message like "Waiting for deployment... rollout to finish" that never progresses.
- Press Ctrl+C to stop watching and get your terminal back.
Why does it hang?
kubectl rollout status waits until ALL new pods are ready. Since the new pods can never pull their image, the command never completes. This is your first signal that something went wrong.
Diagnose the failure
Now that you know the rollout is stuck, dig into what is actually happening with your pods.
- List your pods by running this command:
kubectl get pods
You should see some pods with STATUS ImagePullBackOff or ErrImagePull. These are the new pods that Kubernetes tried to create with the broken image.
Look closely at the list. Some of your original pods are STILL in Running status. Kubernetes did not kill all your replicas before verifying the new ones work.
Why are some old pods still alive?
Kubernetes uses a rolling update strategy by default. It only replaces pods a few at a time, and it waits for new pods to become healthy before killing old ones.
The default maxUnavailable is 25%, which means Kubernetes will never take down more than 25% of your desired replicas at once. When the new pods fail to start, Kubernetes stops the rollout and leaves the surviving old pods running. Your app is still partially serving traffic right now.
- Pick any pod name from the list that shows ImagePullBackOff status.
- Inspect that pod's events by running this command (replace the pod name with yours):
kubectl describe pod [[POD_NAME="name of a pod in ImagePullBackOff status"]]
Scroll down to the Events section at the bottom. You will see error messages explaining that the image could not be pulled from the registry.
What does kubectl describe show?
kubectl describe pod gives you the full story of what happened to a specific pod. The Events section at the bottom shows every action Kubernetes took, in order. You will see it tried to pull nginx:9.99.99, failed, and started backing off (waiting longer between retries).
All pods show Running?
If all your pods still show Running status, the rollout might not have progressed yet. Wait 10-15 seconds and run kubectl get pods again. Kubernetes needs a moment to attempt the image pull and register the failure.
Help me diagnose why my pods are not showing ImagePullBackOff after pushing nginx:9.99.99.
This is the most important lesson in this project. Kubernetes does not blindly replace all your running pods when an update fails. It stops the rollout and keeps your old pods serving traffic. But you are still stuck with a broken deployment. In the next step, you will roll back to the last working version and recover in seconds.
Roll Back and Recover
Your deployment is currently broken. Some pods are stuck in ImagePullBackOff while the surviving replicas keep your app barely alive.
Kubernetes tracks every deployment change as a numbered revision. In this step, you will roll back to the last working version and confirm full recovery in seconds.
In this step, get ready to:
- Roll back the deployment to its previous working state.
- Verify all pods are Running and the app is serving traffic in your browser.
- Inspect the rollout history to see how Kubernetes tracks revisions.
Roll back the deployment to the previous working version
The kubectl rollout undo command tells Kubernetes to revert your deployment to its last known-good revision. No manual YAML editing needed.
Before you run this, what do you think will happen to the pods stuck in ImagePullBackOff? Will they instantly disappear, or will Kubernetes gradually replace them?
- Roll back your deployment by running this command:
kubectl rollout undo deployment/nginx-app
- Check the rollout status by running:
kubectl rollout status deployment/nginx-app
You should see deployment "nginx-app" successfully rolled out. Kubernetes terminated the broken pods and replaced them with the working nginx:1.14.2 image.
What just happened?
Kubernetes stores a revision history for every deployment. When you ran kubectl rollout undo, it looked up the previous revision (which used nginx:1.14.2) and triggered a rolling update back to that image.
The broken pods were replaced gradually, just like any other rolling update. The entire recovery took seconds.
Still seeing pods in ImagePullBackOff?
If kubectl rollout status hangs or pods remain in a failed state, make sure you typed the deployment name exactly as nginx-app. Run kubectl get deployments to confirm the name.
Help me troubleshoot my Kubernetes rollback
Verify the recovery
- Confirm all pods are healthy by running:
kubectl get pods
You should see 4 pods, all with STATUS Running. No more ImagePullBackOff or ErrImagePull in sight.
Now for the real test. Do you think your browser will show the nginx welcome page, or will the rollback have broken the Service connection?
- Open http://localhost:30080 in your browser.
The "Welcome to nginx!" page is back. Your app has fully recovered from the broken deployment.
Why did the Service still work?
Your Service selects pods using the label app: nginx-app. The rollback created new pods with the same label, so the Service automatically routes traffic to them. No Service reconfiguration needed.
Browser not loading the page?
Make sure you are visiting http://localhost:30080 (not https). If the page still does not load, run kubectl get services and confirm nginx-service shows NodePort 30080.
Help me get my nginx welcome page back after rollback
Inspect the rollout history
Kubernetes does not just undo. It keeps a full version history of your deployment, so you can roll back to any previous revision.
- View the revision history by running:
kubectl rollout history deployment/nginx-app
You should see a table of revision numbers. Each revision represents a version of your deployment that Kubernetes tracked. You can roll back to any of them.
What do the revision numbers mean?
Every time you apply a change to a deployment (whether through kubectl apply or kubectl set image), Kubernetes increments the revision number. This is how it knows what "previous version" means when you run kubectl rollout undo.
To roll back to a specific revision (not just the previous one), you could run kubectl rollout undo deployment/nginx-app --to-revision=1.
You have now completed the full deployment lifecycle: deploy, expose, scale, break, and recover. You have seen firsthand that Kubernetes protects your running application and gives you instant rollback when things go wrong. Ready for a bonus challenge? The Secret Mission adds health probes so Kubernetes can detect problems before you even notice them.
Secret mission
Add Health Checks with Liveness and Readiness Probes
Right now, Kubernetes only knows your app is broken if the container process crashes entirely. But what if your app is running yet completely unresponsive? In this mission, you will add liveness and readiness probes so Kubernetes can detect subtle failures and automatically recover without waiting for a full crash.
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 on Docker Desktop, so there are no ongoing cloud costs.
Resources you used:
- A Kubernetes Deployment (nginx-app) running 4 pods.
- A Kubernetes Service (nginx-service) exposing NodePort 30080.
- A local project directory at ~/k8s-first-deploy containing your YAML manifests.
- Docker Desktop with Kubernetes enabled, consuming memory and CPU on your Mac.
Keep everything running
No action needed. Choose this if you are still actively experimenting with Kubernetes or want to try more commands against your running cluster.
- Leave Docker Desktop running with Kubernetes enabled.
- Your pods continue to serve traffic at http://localhost:30080.
- Your YAML files remain in ~/k8s-first-deploy for future use.
Pause - I'll come back to this later
Shut down Docker Desktop to free up memory and CPU. Your YAML files stay on disk, and everything restarts when you relaunch Docker Desktop.
- Click the Docker Desktop icon in your menu bar.
- Click Quit Docker Desktop.
- All pods and services stop. When you relaunch Docker Desktop, Kubernetes restarts and your deployments resume automatically.
- Your YAML files remain in ~/k8s-first-deploy so you can re-apply them at any time.
Delete - I don't want to use this again
Remove all Kubernetes resources and delete your project files from disk.
- Delete the Deployment and Service from your cluster by running:
kubectl delete -f deployment.yaml -f service.yaml
- Confirm the resources are gone by running:
kubectl get pods
kubectl get services
You should see no pods from nginx-app and no nginx-service listed.
- Delete the project directory by running:
rm -rf ~/k8s-first-deploy
- If you want to disable Kubernetes entirely, open Docker Desktop, go to Settings, select Kubernetes, and uncheck Enable Kubernetes. Click Apply & restart.
Nice Work!
Nice Work!
You just deployed, scaled, broke, and recovered a containerized app on a real Kubernetes cluster running on your own machine.
You've learned how to:
- Write Deployment and Service manifests in YAML and apply them with kubectl to run a containerized web server on your local cluster.
- Scale a deployment to multiple replicas and observe Kubernetes load-balance traffic across pods with unique IPs.
- Push a broken update, watch Kubernetes protect running pods from the failure, and recover instantly with rollback.
- Secret Mission: Add liveness and readiness probes so Kubernetes auto-detects unhealthy pods and stops routing traffic to them.
Ready to quiz yourself?