Deploy and Scale a Web App on Kubernetes
Deploy an nginx app to Kubernetes, then scale, self-heal, and roll it back.
Introduction
30 Second Summary
Every time you open an app or load a website, there are servers behind the scenes keeping things running. When those servers crash or get overwhelmed by traffic, something needs to fix them fast.
In this project, you will deploy a web application to a local Kubernetes cluster, scale it up, watch it self-heal when things break, and perform a zero-downtime rolling update.
What You'll Build
You will deploy an nginx web server to Kubernetes, expose it through a Service, and manage it entirely using declarative YAML manifests and kubectl commands.
By the end of this project, you'll have:
- A running Kubernetes cluster on your local machine with a deployed web app accessible in your browser.
- Hands-on experience scaling replicas, observing self-healing, and performing rolling updates and rollbacks.
- A collection of reusable YAML manifests that demonstrate the core Kubernetes deployment workflow.
- Secret Mission: Set up a Horizontal Pod Autoscaler that automatically scales your deployment based on CPU load.
Are there any prerequisites?
You only need a basic understanding of what containers are. No hands-on Docker experience is required.
Install Docker Desktop and Enable Kubernetes
To deploy applications on Kubernetes, you need a cluster. A cluster is a group of machines (called nodes) that Kubernetes uses to run your containers.
In this step, you will install Docker Desktop, which gives you a fully working single-node Kubernetes cluster on your Windows PC. It also installs kubectl, the command-line tool you will use to interact with your cluster throughout this project.
In this step, get ready to:
- Install Docker Desktop on Windows.
- Enable the built-in Kubernetes cluster.
- Verify your cluster is running with kubectl.
Download and install Docker Desktop
Docker Desktop is an application that runs Docker containers and includes a built-in Kubernetes cluster. The installer also adds kubectl to your system so you can manage the cluster from the command line.
- Open your browser and go to the Docker Desktop download page:
- https://docs.docker.com/desktop/setup/install/windows-install/
- Click the download button to get Docker Desktop Installer.exe.
- Double-click Docker Desktop Installer.exe to run the installer.
- When the installer asks which installation mode you prefer, choose the per-user option. This does not require administrator privileges.
- On the Configuration page, make sure Use WSL 2 instead of Hyper-V is selected.
- Follow the remaining wizard prompts and select Close when the installation finishes.
What is Docker Desktop?
Docker Desktop is a free application (for personal use and learning) that lets you run containers on your local machine. For this project, the important part is its built-in single-node Kubernetes cluster.
By enabling Kubernetes inside Docker Desktop, you get a fully functional cluster without needing any cloud account or virtual machines. Everything runs locally on your PC.
- Press the Windows key to open your search bar.
- Type Docker Desktop and press Enter to open it.
- Wait for Docker Desktop to finish starting. You will see the Docker icon appear in the system tray (bottom-right of your taskbar). When it stops animating, Docker is ready.
First launch prompts
Docker Desktop may display a Docker Subscription Service Agreement on first launch. Accept it to continue. This project uses the free Personal tier, which covers personal use and learning.
Docker Desktop won't start?
Make sure hardware virtualization is enabled in your BIOS settings. Docker Desktop requires either Hyper-V or WSL 2 to run containers. Most modern PCs have virtualization enabled by default, but some manufacturers ship it turned off.
help me troubleshoot Docker Desktop failing to start on Windows
Enable Kubernetes in Docker Desktop
Docker Desktop ships with Kubernetes disabled by default. You need to turn it on in the settings so that it downloads the required images and starts the cluster.
- In Docker Desktop, click the gear icon in the top-right to open Settings.
- Select Kubernetes in the left sidebar.
- Check the box labeled Enable Kubernetes.
- Click Apply & Restart.
- Wait for the green indicator and the message Kubernetes running to appear in the Kubernetes settings panel. This takes 2-5 minutes on first run as it downloads the required images.
What happens when you enable Kubernetes?
Docker Desktop downloads several container images that make up the Kubernetes control plane. It then starts a single-node cluster on your machine. This node acts as both the control plane (which makes scheduling decisions) and the worker (which runs your application containers).
The cluster uses approximately 2 GB of RAM while running.
Kubernetes stays on 'Starting' for a long time?
The first-time download can be slow on limited bandwidth. If it takes more than 10 minutes, try clicking Reset Kubernetes Cluster in the same settings panel to retry. Make sure Docker Desktop itself has fully started before enabling Kubernetes.
help me fix Kubernetes stuck on Starting in Docker Desktop
Verify your cluster is running
Now that Kubernetes is enabled, you can confirm it is running by using kubectl in the terminal. Docker Desktop automatically installed kubectl and added it to your PATH.
- Press the Windows key to open your search bar.
- Type PowerShell and press Enter to open it.
- Check that your cluster is reachable by running this command:
kubectl cluster-info
- You should see output showing the Kubernetes control plane address (something like https://127.0.0.1:6443).
- Now check the status of your node by running:
kubectl get nodes
You should see a single node named docker-desktop with STATUS Ready.
✔️ I see docker-desktop with Ready status
Your local Kubernetes cluster is up and running. You have a single-node cluster ready to accept deployments.
ⓧ kubectl is not recognized or not found
Docker Desktop adds kubectl to your PATH during installation, but your terminal session may not have picked it up yet.
- Close your PowerShell window completely.
- Open a new PowerShell window (press the Windows key, type PowerShell, press Enter).
- Run kubectl get nodes again.
If it still does not work, confirm Docker Desktop is running and that you checked Enable Kubernetes in Settings.
help me fix kubectl not found on Windows after installing Docker Desktop
Your local cluster is live and responding to commands. Next up, you will write a Deployment manifest and launch your first application on Kubernetes.
Deploy Your First Application
Your Kubernetes cluster is up and running on your local machine. Now it's time to deploy your first application to it.
You'll write a Deployment manifest that declares exactly what you want running. Kubernetes reads this file and automatically creates the Pods to match your desired state.
In this step, get ready to:
- Create a project folder and a Deployment manifest file.
- Apply the manifest to your Kubernetes cluster.
- Verify your pods are running.
Create your project folder and deployment manifest
Before creating any Kubernetes resources, you need a dedicated folder to store your manifest files. These YAML files tell Kubernetes exactly what to deploy.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir k8s-project
cd k8s-project
- Confirm the folder exists by running:
ls
You should see an empty folder (no output from ls is expected since the folder is new).
Seeing an error with mkdir?
If you see a message saying the folder already exists, that's fine. Run cd k8s-project to move into the existing folder.
If cd ~/Desktop doesn't work, your home directory may have a non-standard location. Try cd $HOME\Desktop instead.
Help me navigate to my Desktop in PowerShell.
Now you'll create the Deployment manifest. This file describes the application you want Kubernetes to run.
- Create your Deployment manifest by pasting the following code into a new file in your text editor:
# Define the API version and resource type
apiVersion: apps/v1
kind: Deployment
metadata:
# Name this deployment so you can reference it later
name: nginx-deployment
labels:
app: nginx
spec:
# Run 2 identical copies of the container
replicas: 2
# Tell the Deployment which pods it manages
selector:
matchLabels:
app: nginx
# Define what each pod looks like
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
# Use the official nginx web server image
image: nginx:1.14.2
ports:
- containerPort: 80
What does this manifest do?
This YAML file is a declaration of your desired state. You're telling Kubernetes: "I want 2 pods, each running an nginx web server on port 80."
- replicas: 2 tells Kubernetes to always maintain exactly 2 copies of your pod.
- selector and labels connect the Deployment to its pods using the app: nginx label.
- image: nginx:1.14.2 specifies the exact container image each pod will run.
- Save the file as deployment.yaml inside your k8s-project folder.
Saving in your text editor
If you're using VS Code, you can run code . in your terminal to open the k8s-project folder directly. Then create a new file and save it as deployment.yaml.
If you're using Notepad, make sure to change the "Save as type" dropdown to "All Files" and type the full filename deployment.yaml (not deployment.yaml.txt).
- Confirm the file exists by running:
ls
You should see deployment.yaml listed in the output.
Don't see deployment.yaml?
Make sure you saved the file inside the k8s-project folder on your Desktop, not somewhere else.
If using Notepad, double-check that the file wasn't saved as deployment.yaml.txt. Change "Save as type" to "All Files" and try again.
Help me save my deployment.yaml file in the right location.
Now you'll send this manifest to your Kubernetes cluster.
- Apply the Deployment to your cluster by running:
kubectl apply -f deployment.yaml
You should see the output: deployment.apps/nginx-deployment created.
Seeing an error about the file not being found?
Make sure your terminal is inside the k8s-project folder. Run cd ~/Desktop/k8s-project to get there.
If you see a YAML parsing error, check that your file uses spaces for indentation (not tabs) and that the structure matches the code above exactly.
Help me fix my kubectl apply error.
✔️ Awesome, I've got everything!
Great. Make sure you've saved deployment.yaml and that kubectl apply returned a success message.
ⓧ I'd like to double check the full code
Here is the complete deployment.yaml file for comparison:
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
labels:
app: nginx
spec:
replicas: 2
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.14.2
ports:
- containerPort: 80
Verify the deployment is running
Your manifest told Kubernetes to create 2 pods. Let's confirm they're up and healthy.
- Check the status of your Deployment by running:
kubectl get deployments
You should see nginx-deployment with 2/2 in the READY column.
- Check the individual pods by running:
kubectl get pods
You should see 2 pods with STATUS Running. Each pod name starts with nginx-deployment- followed by a random string.
Pods not showing Running?
If your pods show ContainerCreating, wait 30 seconds and run kubectl get pods again. Kubernetes is pulling the nginx image for the first time.
If pods show ErrImagePull or ImagePullBackOff, check that Docker Desktop is running and has internet access.
Help me fix my pods that aren't starting.
- Inspect one of your pods for more detail by running:
kubectl describe pod [[POD_NAME="a pod name from the output above"]]
What does kubectl describe show?
The describe command shows you everything about a pod: its labels, which node it runs on, container status, and recent events. This is your go-to command for debugging pods that aren't behaving as expected.
What happened behind the scenes?
When you applied the manifest, the Deployment created a ReplicaSet. The ReplicaSet then created 2 Pods.
Each Pod runs a single nginx container. Kubernetes will maintain exactly 2 replicas at all times. If a pod crashes or gets deleted, the ReplicaSet automatically creates a replacement.
Your first application is deployed and running. Next up, you'll expose it as a Service so you can access it in your browser.
Expose Your App with a Service
Your nginx pods are running, but there is no way to reach them from outside the cluster. Pods get internal IP addresses that change every time they restart, so you cannot rely on them directly.
A Service solves this by providing a stable network endpoint that routes traffic to your pods. In this step, you will create a Service manifest and use it to expose your nginx app on localhost so you can view it in your browser.
In this step, get ready to:
- Create a Service YAML manifest that targets your nginx pods.
- Access your running app in the browser at http://localhost:80.
- Verify the Service is routing traffic to both pods.
Create a Service manifest
A LoadBalancer Service type creates an external-facing entry point to your cluster. On Docker Desktop, LoadBalancer Services are automatically mapped to localhost, so you can access them directly in your browser.
- Create a new file named service.yaml inside your k8s-project folder using your text editor.
- Paste the following YAML into the file:
# Service to expose nginx pods outside the cluster
apiVersion: v1
kind: Service
metadata:
name: nginx-service
spec:
# Route traffic to pods with this label
selector:
app: nginx
ports:
- protocol: TCP
port: 80 # Port the Service listens on
targetPort: 80 # Port on the container
# LoadBalancer maps to localhost on Docker Desktop
type: LoadBalancer
What does this manifest do?
- selector: app: nginx tells the Service to find all pods with the label app: nginx and route traffic to them.
- port: 80 is the port you will use to access the Service. targetPort: 80 is the port your nginx container is listening on.
- type: LoadBalancer requests an external IP. Docker Desktop intercepts this and maps it to localhost.
- Save service.yaml.
- Apply the Service to your cluster by running this command:
kubectl apply -f service.yaml
- You should see output saying service/nginx-service created.
Seeing an error about the file not found?
Make sure your terminal is inside the k8s-project folder where you saved service.yaml. Run cd k8s-project if you navigated away.
Also confirm the file is named exactly service.yaml with no extra extension like .txt.
help me fix a kubectl apply file not found error
Access your app in the browser
- Check that the Service is active by running:
kubectl get services
You should see nginx-service listed with TYPE LoadBalancer and EXTERNAL-IP showing as localhost.
Why does EXTERNAL-IP say localhost?
Docker Desktop includes a built-in cloud-provider simulation. When you create a LoadBalancer Service, Docker Desktop provisions it to localhost instead of a real cloud IP. This lets you test LoadBalancer Services locally without needing a cloud provider.
- Open your web browser.
- Navigate to http://localhost:80.
You should see the "Welcome to nginx!" page. This confirms your Service is routing external traffic to your nginx pods.
Not seeing the nginx welcome page?
Make sure Docker Desktop is still running (check for the whale icon in the system tray).
If another application is using port 80 on your machine, the Service cannot bind to it. Close any local web servers (Apache, IIS, or other apps on port 80) and try again.
help me troubleshoot why localhost:80 is not showing my nginx page
Verify the Service is routing to your pods
The Endpoints field in a Service description shows the internal IP addresses of the pods receiving traffic. Since you have 2 running pods, you should see 2 endpoints.
- Inspect your Service details by running:
kubectl describe service nginx-service
In the output, find the Endpoints line. You should see 2 IP addresses separated by a comma (one for each pod).
What do Endpoints tell you?
Each endpoint is the internal IP of a pod that matches the Service's selector. The Service load-balances incoming requests across all listed endpoints.
If a pod crashes and a new one is created, the endpoints update automatically. You never need to reconfigure the Service.
✔️ Awesome, I've got everything!
Great. Double check you have saved service.yaml and that http://localhost:80 loads the nginx page.
ⓧ I'd like to double check the full code
Here is the complete service.yaml file:
apiVersion: v1
kind: Service
metadata:
name: nginx-service
spec:
selector:
app: nginx
ports:
- protocol: TCP
port: 80
targetPort: 80
type: LoadBalancer
Your app is live and reachable from outside the cluster. Next up, you will scale your deployment to more replicas and watch Kubernetes automatically heal itself when pods are deleted.
Scale and Test Self-Healing
Your nginx app is live and accessible through a Service. Right now, two pods are handling all the traffic. But what happens if traffic spikes, or a pod crashes?
This is where Kubernetes really shines. You can scale your deployment up or down with a single command. If a pod dies, Kubernetes automatically replaces it to maintain your desired replica count.
In this step, get ready to:
- Scale your deployment from 2 to 5 replicas.
- Delete a pod and watch Kubernetes self-heal.
- Scale back down to 3 replicas.
Scale your deployment up
Your deployment currently manages 2 replicas. Scaling up means telling Kubernetes to run more copies of your pod to handle additional load.
- Scale the deployment to 5 replicas by running this command:
kubectl scale deployment/nginx-deployment --replicas=5
What does this command do?
The kubectl scale command tells Kubernetes to change the number of running pods for a deployment. Kubernetes compares the current state (2 pods) to the desired state (5 pods) and spins up the difference.
- Verify the new pods are running by running:
kubectl get pods -l app=nginx
You should see 5 pods listed. Some may briefly show a status of ContainerCreating before switching to Running. All 5 should reach Running within a few seconds.
Still seeing fewer than 5 pods?
Wait a few seconds and run kubectl get pods -l app=nginx again. Pod creation depends on pulling the container image, which may take a moment on slower machines.
If pods show Pending status for more than 30 seconds, your machine may be low on resources. Check that Docker Desktop has at least 4 GB of memory allocated in Settings > Resources.
Help me troubleshoot pods stuck in Pending state.
Test self-healing by deleting a pod
Kubernetes constantly compares the actual number of running pods against the desired replica count. If a pod disappears for any reason, Kubernetes immediately creates a replacement.
- Copy any one of the pod names from the output of your previous kubectl get pods command.
- Delete that pod by running:
kubectl delete pod [[POD_NAME="paste your pod name here e.g. nginx-deployment-abc12-xyz34"]]
- Immediately run this command to watch Kubernetes react:
kubectl get pods -l app=nginx
You should still see 5 pods. One will be brand new (with a younger AGE value or a ContainerCreating status). That is the replacement pod Kubernetes created automatically.
What is self-healing?
The Deployment controller runs a continuous loop. It checks the desired replica count (5) against the actual count. When you deleted a pod, the actual count dropped to 4. The controller detected this discrepancy and immediately scheduled a new pod to restore the count to 5.
This same mechanism protects your app if a pod crashes due to a bug or a node runs out of memory. Kubernetes replaces failed pods without any manual intervention.
Seeing an error when deleting the pod?
Make sure you copied the full pod name exactly as shown in the kubectl get pods output. Pod names include a random suffix like nginx-deployment-abc12-xyz34. A typo or missing character will cause a "not found" error.
Help me fix my kubectl delete pod command.
Scale back down
In production, you would scale down after a traffic spike passes to free up cluster resources. Kubernetes gracefully terminates the extra pods.
- Scale the deployment down to 3 replicas by running:
kubectl scale deployment/nginx-deployment --replicas=3
- Confirm the scale-down by running:
kubectl get pods -l app=nginx
You should see exactly 3 pods with status Running. The other 2 pods will show Terminating briefly before disappearing from the list entirely.
Why 3 replicas as the final count?
Three replicas is a common baseline for production workloads. It provides redundancy (one pod can fail without downtime) while keeping resource usage reasonable. You can always adjust this number based on your actual traffic needs.
Still seeing more than 3 pods?
Pods in Terminating state can linger for up to 30 seconds while Kubernetes gracefully shuts them down. Wait a moment and run kubectl get pods -l app=nginx again.
Help me understand why my pods are stuck in Terminating state.
- Refresh http://localhost:80 in your browser to confirm the app is still accessible.
The nginx welcome page still loads because the Service automatically routes traffic to whichever pods are currently running.
You have proven that Kubernetes keeps your app running at the desired scale, even when pods are destroyed. Next up, you will perform a rolling update to change your app version with zero downtime.
Perform a Rolling Update and Rollback
Your deployment can now scale up and heal itself when pods are deleted. But what happens when you need to ship a new version of your app?
In production, you cannot take your app offline to swap in a new version. Kubernetes solves this with rolling updates. It gradually replaces old pods with new ones so your app stays available throughout the process.
In this step, get ready to:
- Trigger a rolling update to a newer nginx version.
- Verify the update completed with zero downtime.
- Roll back to the previous version in a single command.
Trigger a rolling update
A rolling update tells Kubernetes to replace your current pods with pods running a new container image. Kubernetes does this gradually, so there is always at least one pod serving traffic.
- Trigger the update by changing the container image to nginx:1.16.1:
kubectl set image deployment/nginx-deployment nginx=nginx:1.16.1
What does this command do?
This tells Kubernetes to update the container named nginx inside your nginx-deployment to use the nginx:1.16.1 image instead of nginx:1.14.2.
By default, Kubernetes uses a RollingUpdate strategy. It replaces pods a few at a time, ensuring at least 75% of your desired replicas stay available throughout the process.
- Watch the rollout happen in real time by running:
kubectl rollout status deployment/nginx-deployment
You should see messages about replicas being updated. Once complete, the output reads deployment "nginx-deployment" successfully rolled out.
- See the pods being replaced by running:
kubectl get pods
You should see new pods with a recent AGE value. If you run this command quickly enough during the update, you may catch old pods in a Terminating state and new pods in ContainerCreating.
Rollout seems stuck?
If kubectl rollout status hangs without completing, your cluster may not have enough resources to schedule new pods. Run kubectl get pods to check if any pods are stuck in Pending state.
Make sure Docker Desktop has at least 2GB of memory allocated in Settings > Resources.
Help me troubleshoot my rolling update that seems stuck
Verify the update
- Confirm the new image is active by running:
kubectl describe deployment nginx-deployment
In the output, look for the Pod Template section. The Image field should now show nginx:1.16.1.
- Open your browser and refresh http://localhost:80.
The nginx welcome page still loads. Your app stayed available the entire time the update was happening. That is zero-downtime deployment in action.
Why does the page look the same?
The default nginx welcome page is identical across minor versions. The underlying server binary changed from 1.14.2 to 1.16.1, but the HTML it serves did not. In a real project, a version bump would deploy new application code with visible changes.
Roll back to the previous version
Sometimes a new version introduces a bug and you need to revert immediately. Kubernetes tracks your deployment's revision history, so a rollback is a single command away.
- Roll back to the previous version by running:
kubectl rollout undo deployment/nginx-deployment
What does this command do?
This tells Kubernetes to revert your deployment to the previous revision. It performs another rolling update, but this time it replaces the nginx:1.16.1 pods with pods running nginx:1.14.2 again.
The rollback uses the same gradual replacement strategy. Your app stays available during the revert.
- Watch the rollback complete by running:
kubectl rollout status deployment/nginx-deployment
Wait until you see deployment "nginx-deployment" successfully rolled out.
- Confirm the image is back to the original version by running:
kubectl describe deployment nginx-deployment
The Image field under Pod Template should show nginx:1.14.2 again.
- View the full rollout history by running:
kubectl rollout history deployment/nginx-deployment
You should see at least 2 revisions listed. Each revision represents a change you made to the deployment's pod template.
Only seeing 1 revision?
If you only see a single revision, the rollback may not have completed. Run kubectl rollout status deployment/nginx-deployment again and wait for the success message before checking history.
Also confirm you ran the kubectl set image command earlier in this step. Without the initial update, there is no second revision to display.
Help me figure out why my rollout history only shows one revision
Secret mission
Set Up a Horizontal Pod Autoscaler
Your deployment scales manually right now. What if it could respond to real traffic automatically? In this secret mission, you'll configure a Horizontal Pod Autoscaler that watches CPU usage and scales your pods up and down without any manual intervention.
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 cloud costs.
Resources you used:
- Docker Desktop Kubernetes cluster (running as containers on your machine, uses approximately 2GB RAM).
- Kubernetes workloads: nginx-deployment, nginx-service, HPA, and metrics-server.
- Project folder k8s-project with YAML manifest files.
Keep everything running
No action needed. Choose this if you are still actively building or experimenting with your cluster.
- Docker Desktop continues running your Kubernetes cluster in the background.
- Your deployment, service, and HPA remain active and accessible at http://localhost:80.
- This uses approximately 2GB of RAM on your machine.
Pause - I'll come back to this later
Shut down Docker Desktop to free up memory. Your cluster state is preserved and will resume when you reopen Docker Desktop.
- Right-click the Docker Desktop whale icon in the system tray.
- Select Quit Docker Desktop.
- When you are ready to resume, open Docker Desktop again from the Start menu. Your cluster and workloads will restart automatically.
Delete - I don't want to use this again
Remove all project resources and disable Kubernetes entirely.
- Delete your Kubernetes workloads by running these commands in PowerShell:
kubectl delete hpa nginx-deployment
kubectl delete -f service.yaml
kubectl delete -f deployment.yaml
kubectl delete -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml
- Disable Kubernetes in Docker Desktop:
- Open Docker Desktop.
- Go to Settings, then Kubernetes.
- Uncheck Enable Kubernetes.
- Click Apply.
- Delete the project folder by navigating to its parent directory and running:
cd ..
Remove-Item -Recurse -Force k8s-project
- Confirm the folder is gone by running ls and verifying k8s-project no longer appears.
Nice Work!
Nice Work!
You just deployed, scaled, updated, and recovered a web application on a local Kubernetes cluster. That is the core workflow behind every production Kubernetes environment.
You've learned how to:
- Write declarative YAML manifests and deploy nginx pods to a local Kubernetes cluster using kubectl apply.
- Expose your app with a LoadBalancer Service, scale replicas up and down, and observe Kubernetes self-healing when pods are deleted.
- Perform a zero-downtime rolling update to a new container image and roll back to a previous version with a single command.
- Secret Mission: Configured a Horizontal Pod Autoscaler that automatically scales pods based on CPU utilization.
Ready to quiz yourself?