Deploy Your First App on Kubernetes
Set up a local Kubernetes cluster and deploy a web app with minikube.
Introduction
30 Second Summary
Think about how apps like Netflix or Spotify stay online even when servers crash. Behind the scenes, a system called Kubernetes automatically detects problems and fixes them without anyone lifting a finger.
In this project, you will set up your own Kubernetes cluster on your Mac, deploy a web server, and then push it to its limits by breaking things on purpose to see how Kubernetes heals itself, scales up, and performs zero-downtime updates.
What You'll Build
A local Kubernetes cluster running a multi-replica nginx web server that you can scale, update, and break on command.
By the end of this project, you'll have:
- A running local Kubernetes cluster with a deployed web application accessible in your browser.
- Hands-on experience with Kubernetes self-healing by deleting Pods and watching them recreate automatically.
- A scaled deployment running 5 replicas behind a load-balanced Service with zero-downtime rolling updates.
- Secret Mission: Deploy a second application with injected configuration using a ConfigMap.
Are there any prerequisites?
You just need basic comfort with the terminal (running commands, navigating directories). No prior Kubernetes or Docker knowledge is required.
Install Docker Desktop, minikube, and kubectl
Before you can run a Kubernetes cluster, you need the software that makes it possible.
In this step, you will install three tools: Docker Desktop (the container runtime), minikube (which creates a local Kubernetes cluster), and kubectl (which lets you talk to the cluster from your terminal).
In this step, get ready to:
- Install Docker Desktop as the container runtime for your cluster.
- Install minikube and kubectl using Homebrew.
- Start your first Kubernetes cluster and confirm it is healthy.
Install Docker Desktop for macOS
Docker Desktop provides the container engine that minikube uses to run your cluster nodes.
- Open your browser and go to the Docker Desktop download page:
- https://www.docker.com/products/docker-desktop/
- Click Download for Mac – Apple Silicon if you have an M-series chip, or Download for Mac – Intel Chip if you have an Intel Mac.
- Double-click the downloaded Docker.dmg file to open the installer.
- Drag the Docker icon into the Applications folder.
- Open Docker Desktop from your Applications folder by double-clicking Docker.app.
- Accept the Docker Subscription Service Agreement when prompted.
- Wait for the Docker engine to start. You will see a green status indicator in the Docker Desktop window.
Why Docker Desktop?
Minikube needs a container runtime to create the virtual nodes that form your cluster. On macOS, Docker Desktop is the preferred driver because it runs containers natively without needing a separate hypervisor.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Terminal and press Enter to open it.
- Verify Docker is installed by running:
docker --version
✔️ I see a version number
Docker is installed and ready. You are good to move on to the next tool.
ⓧ Command not found
This means Docker Desktop is not fully installed or is not in your PATH yet.
- Make sure you dragged the Docker icon into the Applications folder (not just double-clicked it from the .dmg).
- Open Docker.app from your Applications folder and wait for the engine to finish starting.
- Close and reopen your Terminal window, then try the command again.
Still seeing command not found?
help me troubleshoot why docker --version is not working on my Mac after installing Docker Desktop
Install minikube and kubectl with Homebrew
Homebrew is the standard package manager for macOS. You will use it to install both minikube and kubectl.
- Check if Homebrew is already installed by running:
brew --version
✔️ I see a version number
Homebrew is installed. Skip ahead to the minikube install command below.
ⓧ Command not found
You need to install Homebrew first.
- Install Homebrew by running this command in your terminal:
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
What does this command do?
This downloads and runs the official Homebrew installer script. It will explain what it plans to do and ask you to confirm before making any changes.
The installer may ask for your Mac password. It may also prompt you to install Xcode Command Line Tools if they are not already present.
- Follow any on-screen instructions from the installer. If it tells you to run additional commands to add Homebrew to your PATH, do so now.
- Close and reopen your Terminal, then verify with brew --version.
- Install minikube by running:
brew install minikube
- Install kubectl by running:
brew install kubectl
What are minikube and kubectl?
Minikube creates a single-node Kubernetes cluster on your local machine. It runs entirely inside Docker containers, so there are no cloud costs.
Kubectl is the command-line tool for sending instructions to any Kubernetes cluster. Every time you deploy an app, scale replicas, or check on your Pods, you use kubectl.
- Verify minikube is installed by running:
minikube version
- Verify kubectl is installed by running:
kubectl version --client
✔️ Both commands show version numbers
Both tools are installed and ready. Time to start your cluster.
ⓧ One or both commands show command not found
- Make sure Homebrew is in your PATH. Close and reopen your Terminal and try again.
- If the install command itself failed, try running brew update first, then retry the install command.
Still not working?
help me troubleshoot why brew install minikube or kubectl is failing on my Mac
Start your first Kubernetes cluster
With all three tools installed, you can now create your local Kubernetes cluster.
- Make sure Docker Desktop is still running. You should see the Docker whale icon in your Mac menu bar.
- Start your cluster by running:
minikube start --driver=docker
What does this command do?
This tells minikube to create a new Kubernetes cluster using Docker as the driver. Minikube downloads the necessary Kubernetes components and starts them inside a Docker container.
The first time you run this, it downloads several container images. This can take 2-5 minutes depending on your internet speed.
- Once the command finishes, verify your cluster is healthy by running:
kubectl get nodes
You should see one node listed with a status of Ready. This means your cluster is up and running.
Don't see a Ready node?
- Make sure Docker Desktop is running (check for the whale icon in the menu bar).
- If you see an error about the Docker driver, try stopping Docker Desktop and restarting it, then run minikube start --driver=docker again.
- If the node shows NotReady, wait 30 seconds and run kubectl get nodes again. The cluster may still be starting up.
help me troubleshoot why minikube start is failing with Docker on my Mac
Your local Kubernetes cluster is alive. Next up, you will deploy your first application onto it.
Deploy Your First Application
Your cluster is running and healthy. Now it's time to put something on it.
In this step, you'll deploy an nginx web server onto your Kubernetes cluster using a Deployment. You'll then trace how Kubernetes organizes your application behind the scenes and learn to inspect your running workloads.
In this step, get ready to:
- Create a Deployment that runs an nginx web server.
- Trace the Deployment → ReplicaSet → Pod chain that Kubernetes creates automatically.
- Inspect your Pod's logs and details to see what's happening inside.
Create a Deployment running nginx
A Deployment tells Kubernetes what application you want to run and how many copies of it to keep alive. Kubernetes takes care of actually creating and managing the containers.
- Create a Deployment named nginx-app that runs an nginx web server by running this command:
kubectl create deployment nginx-app --image=nginx:1.27
What does this command do?
This command tells Kubernetes to create a Deployment called nginx-app that runs a container using the nginx:1.27 image from Docker Hub.
Kubernetes pulls the image, creates a container from it, and wraps it in a Pod. The Deployment watches over that Pod and ensures it stays running.
- You should see deployment.apps/nginx-app created in your terminal output.
- Inspect the Deployment by running:
kubectl get deployments
- Check the Pod that was created by running:
kubectl get pods
You should see one Pod with a name starting with nginx-app and a STATUS of Running. The READY column should show 1/1.
Pod stuck in ContainerCreating or Pending?
If the STATUS column shows ContainerCreating, Kubernetes is still pulling the nginx image. Wait 30 seconds and run kubectl get pods again.
If the Pod shows ErrImagePull or ImagePullBackOff, make sure Docker Desktop is running and your internet connection is active.
help me debug my Pod that isn't reaching Running status
Trace the Deployment chain
When you created the Deployment, Kubernetes didn't just create a Pod directly. It created a chain of objects that work together to keep your application running.
- View the ReplicaSet that the Deployment created by running:
kubectl get replicasets
- Get a detailed view of the Deployment's configuration by running:
kubectl describe deployment nginx-app
What is the Deployment chain?
The Deployment created a ReplicaSet, which in turn created a Pod, which runs your nginx container. Each layer has a job:
- Deployment manages updates and rollbacks.
- ReplicaSet ensures the right number of Pods are always running.
- Pod is the smallest deployable unit. It wraps one or more containers.
In the kubectl describe deployment output, look for the NewReplicaSet field. This shows the ReplicaSet that the Deployment is currently managing.
Inspect your Pod
Every Pod has logs and detailed metadata. These are your primary tools for understanding what's happening inside a running container.
- Copy your Pod's full name from the kubectl get pods output you ran earlier. It looks something like nginx-app-xxxxxxxxxx-xxxxx.
- View the Pod's logs by running this command, replacing the Pod name with yours:
kubectl logs [[PODNAME="your-pod-name-from-kubectl-get-pods"]]
What should I see?
The logs may be empty or show a few startup lines from nginx. That's normal. Since no one has sent any web traffic to this server yet, there's nothing to log.
- Get detailed information about your Pod by running:
kubectl describe pod [[PODNAME="your-pod-name-from-kubectl-get-pods"]]
What does this output tell me?
The kubectl describe pod output shows you everything about the Pod's lifecycle:
- Status confirms the container is running.
- IP shows the Pod's internal IP address within the cluster.
- Events (at the bottom) shows the timeline of actions Kubernetes took to get this Pod running.
Command says error: pod not found?
Make sure you copied the full Pod name exactly as shown in your kubectl get pods output. The name includes random characters at the end and is case-sensitive.
Run kubectl get pods again to get the current Pod name, then retry the command.
help me fix the pod not found error in kubectl
Your nginx web server is running inside the cluster. Next up, you'll expose it with a Service so you can actually see it in your browser.
Expose Your App with a Service
Your nginx app is running inside the cluster, but there is no way to reach it from your browser yet. Pods live on a private network that is invisible to the outside world.
To make your app accessible, you need a Service. A Service provides a stable endpoint that routes traffic into your Pods, even as those Pods are destroyed and recreated.
In this step, get ready to:
- Expose your Deployment as a NodePort Service.
- Access the nginx welcome page in your browser using minikube's service tunnel.
- Inspect the Service to understand how traffic routing works.
Expose your Deployment as a Service
Right now, the only way to talk to your Pod is from inside the cluster. A NodePort Service opens a port on your node so that external traffic can reach your app.
- Create a Service for your nginx-app Deployment by running this command:
kubectl expose deployment nginx-app --type=NodePort --port=80
What does this command do?
- kubectl expose deployment nginx-app tells Kubernetes to create a Service that points at the Pods managed by your nginx-app Deployment.
- --type=NodePort makes the Service reachable from outside the cluster by opening a port on your node.
- --port=80 tells the Service to listen on port 80, which is the port nginx uses inside the container.
- Verify the Service was created by running:
kubectl get services
You should see two entries: the default kubernetes ClusterIP service that always exists, and your new nginx-app NodePort service with a port mapping like 80:<some-port>/TCP.
Don't see your nginx-app Service?
- Make sure you typed the Deployment name exactly as nginx-app. Kubernetes names are case-sensitive.
- If you see an error about the service already existing, it means you ran the expose command twice. Run kubectl delete service nginx-app and try again.
- Help me troubleshoot my Service creation
Open your app in the browser
Because minikube runs inside Docker on macOS, you cannot directly access the NodePort from your host machine. minikube provides a built-in tunnel that opens the Service URL in your browser.
- Open the nginx welcome page in your browser by running:
minikube service nginx-app
A browser tab opens showing the nginx "Welcome to nginx!" page.
Why do I need minikube service?
When using the Docker driver on macOS, minikube's node is a container, not a VM with a routable IP. The minikube service command creates a tunnel from your Mac to the Service's NodePort so your browser can reach it.
Keep the terminal running while you use the app. Pressing Ctrl+C closes the tunnel and the page will stop loading.
Browser tab didn't open?
- Make sure Docker Desktop is still running. The tunnel relies on the Docker network.
- If the command prints a URL but no browser opens, copy the URL from the terminal output and paste it into your browser manually.
- Help me access my nginx Service in the browser
Inspect the Service details
Now that you can reach your app, let's look at how the Service actually routes traffic to your Pod.
- View the full details of your Service by running:
kubectl describe service nginx-app
You should see output showing the Service's Name, Type, Selector, Port, TargetPort, NodePort, and Endpoints fields.
What do these fields mean?
- Selector is the label (like app=nginx-app) the Service uses to find which Pods should receive traffic.
- Endpoints lists the IP addresses of the Pods currently matching that selector. If you scale up later, more IPs appear here automatically.
- NodePort is the external port Kubernetes allocated on your node. This is the port the tunnel maps to your Mac.
- TargetPort is the port on the container where nginx is listening (port 80).
The key insight: Pods can come and go, but the Service always knows where to route traffic because it watches for Pods matching its selector label.
What is a selector?
A selector is a label query. When you created the Deployment with kubectl create deployment nginx-app, Kubernetes automatically added the label app=nginx-app to every Pod it manages.
The Service uses that same label as its selector, which is how it discovers the right Pods to send traffic to.
Your app is now exposed and accessible from your browser. Next up, you will break things on purpose to watch Kubernetes heal itself and scale your app to multiple replicas.
Break Things on Purpose
Your nginx app is deployed and accessible through a Service. You can reach it in the browser and confirm everything works.
Running an app is just the beginning. What happens when a container crashes or traffic spikes? In this step, you'll stress-test Kubernetes by deleting Pods, scaling replicas up, and scaling them back down.
In this step, get ready to:
- Test Kubernetes self-healing by deleting a Pod and watching it come back to life.
- Scale your application from 1 to 5 replicas.
- Scale back down and watch graceful termination.
Test self-healing by deleting a Pod
Kubernetes promises that your application stays running even when individual containers fail. This behavior is called self-healing. The ReplicaSet continuously monitors how many Pods are running and creates replacements whenever the count drops below the desired number.
You're about to prove this by deleting a Pod on purpose and watching Kubernetes fix the problem automatically.
- Get your current Pod name by running this command:
kubectl get pods
You should see one Pod with a name like nginx-app-xxxxxxxxxx-xxxxx in Running status.
- Copy your Pod's full name from the output. You'll need it for the next command.
- Delete the Pod by running:
kubectl delete pod [[POD_NAME="your Pod name, e.g. nginx-app-5d7f8b9c4-x2j7k"]]
You should see a message confirming the Pod was deleted.
- Immediately watch for the replacement Pod by running:
kubectl get pods --watch
You should see the old Pod transition to Terminating and a brand new Pod appear with status Pending, then Running. This happens within seconds.
- Press Ctrl+C to stop the watch once you see the new Pod in Running status.
What just happened?
You told Kubernetes to destroy a Pod. The ReplicaSet controller noticed the actual replica count (0) was below the desired count (1) and immediately created a replacement.
This is self-healing in action. In production, if a container crashes due to a bug or a hardware failure, Kubernetes handles recovery automatically without human intervention.
Pod not found error?
Make sure you copied the full Pod name exactly as shown in the kubectl get pods output. Pod names are case-sensitive and include random characters at the end.
If the Pod already terminated before you ran the delete command, run kubectl get pods again to get the current Pod name.
Help me fix a "pod not found" error when trying to delete my nginx-app Pod
Scale your application to 5 replicas
One Pod handles one portion of traffic. When demand increases, you need more copies of your application running simultaneously. Kubernetes calls each copy a replica.
- Scale your Deployment to 5 replicas by running:
kubectl scale deployment nginx-app --replicas=5
What does this command do?
This tells the ReplicaSet to maintain 5 copies of your nginx container at all times. Kubernetes creates 4 additional Pods to meet the new desired count.
Your Service automatically discovers all 5 Pods and distributes incoming traffic across them. This is load balancing happening without any extra configuration.
- Verify all 5 Pods are running:
kubectl get pods
You should see 5 Pods listed, all with status Running. Each has a unique name but they all belong to the same nginx-app Deployment.
- Access your Service again to confirm it still works by running:
minikube service nginx-app
The same nginx welcome page loads. Behind the scenes, the Service is now load-balancing across all 5 replicas.
Pods stuck in Pending?
If one or more Pods show Pending status for more than 30 seconds, your minikube cluster may not have enough resources allocated. Check Docker Desktop's resource settings and ensure at least 4 GB of memory is assigned.
Help me troubleshoot Pods stuck in Pending status after scaling up
Scale back down to 2 replicas
Scaling up is useful during traffic spikes, but running more replicas than needed wastes resources. Kubernetes lets you scale back down just as easily.
- Scale your Deployment down to 2 replicas by running:
kubectl scale deployment nginx-app --replicas=2
- Watch Kubernetes gracefully terminate the excess Pods by running:
kubectl get pods --watch
You should see 3 Pods transition to Terminating status and disappear, leaving exactly 2 Pods in Running status.
- Press Ctrl+C to stop the watch once you see only 2 Pods remaining.
Why scale down to 2 instead of 1?
Running 2 replicas means your app stays available even if one Pod fails. Kubernetes can serve traffic from the healthy Pod while it recreates the failed one. This is a common production pattern for high availability.
Still see more than 2 Pods?
Pods in Terminating status are being shut down gracefully. Wait a few seconds and run kubectl get pods again. Only 2 Pods should remain in Running status.
Help me troubleshoot Pods that won't terminate after scaling down
You've just proven that Kubernetes can heal itself, scale up under pressure, and scale back down to save resources. Next up, you'll perform a rolling update to change your app's version with zero downtime.
Perform a Rolling Update
Your application just proved it can heal itself and scale on demand. Now there's one more critical production skill to test.
In production, you need to update your application's code or image without taking it offline. Kubernetes handles this through rolling updates, replacing Pods one at a time so your Service never goes down.
In this step, get ready to:
- Trigger a rolling update by changing the container image.
- Observe the rolling update strategy in real time.
- Roll back to the previous image version.
Trigger a rolling update
A rolling update swaps your running Pods to a new container image one at a time. Kubernetes only terminates an old Pod after the new one passes its readiness checks.
- Update the nginx-app Deployment image from nginx:1.27 to nginx:1.27-alpine by running this command:
kubectl set image deployment/nginx-app nginx=nginx:1.27-alpine
What does this command do?
kubectl set image tells Kubernetes to update the container image for a specific Deployment.
The nginx=nginx:1.27-alpine part targets the container named nginx inside the Pod and changes its image to the smaller Alpine-based variant.
- Watch the rollout progress by running:
kubectl rollout status deployment/nginx-app
You should see the message deployment "nginx-app" successfully rolled out once all Pods have been updated.
Seeing ImagePullBackOff or ErrImagePull?
Double-check that you typed the image name exactly as nginx:1.27-alpine. A typo in the image tag causes Kubernetes to fail pulling the image.
If you made a typo, re-run the kubectl set image command with the correct image name. Kubernetes will automatically retry.
Help me fix my rolling update image pull error
Observe the rolling update strategy
The rolling update already completed, but you can still trace exactly what happened. Kubernetes created new Pods with the updated image one at a time, only terminating old Pods after new ones were Ready.
- Open a second terminal window so you can watch Pod changes in real time for the upcoming rollback.
- In the second terminal, start watching Pods by running:
kubectl get pods --watch
Why use a second terminal?
The --watch flag keeps the command running and prints every Pod status change as it happens. Having it in a separate window means you can trigger changes in your first terminal and watch the results stream in live.
- Switch back to your first terminal.
- Verify the update completed by running:
kubectl describe deployment nginx-app
Look for the Image field in the output. It should now show nginx:1.27-alpine.
What is the rolling update strategy?
By default, Kubernetes uses a RollingUpdate strategy. It creates a new Pod with the updated image, waits for it to become Ready, then terminates one old Pod.
This repeats until all replicas run the new image. Your Service keeps routing traffic the entire time, so users experience zero downtime.
Roll back to the original image
Sometimes an update introduces a bug. Kubernetes lets you instantly revert to the previous image version with a single command.
- Roll back the Deployment to its previous state by running:
kubectl rollout undo deployment/nginx-app
What does rollout undo do?
kubectl rollout undo tells Kubernetes to revert the Deployment to its previous revision. Kubernetes performs another rolling update, this time replacing the Alpine Pods with the original nginx:1.27 image.
Watch your second terminal. You should see new Pods being created and old Pods terminating as the rollback progresses.
- Confirm the rollback completed by running:
kubectl rollout status deployment/nginx-app
You should see deployment "nginx-app" successfully rolled out again.
- Verify the image is back to the original by running:
kubectl describe deployment nginx-app
The Image field should now show nginx:1.27 instead of the Alpine variant.
Image still showing alpine?
Make sure you ran kubectl rollout undo deployment/nginx-app and not kubectl rollout undo deployment nginx-app (note the forward slash before the deployment name).
Run kubectl rollout status deployment/nginx-app to check whether the rollback is still in progress or has completed.
Help me troubleshoot my rollback
Secret mission
Deploy a ConfigMap-Driven Second App
So far, you've been using imperative kubectl commands to manage your cluster. In the real world, teams define their infrastructure in YAML files and store them in version control. This secret mission challenges you to write Kubernetes manifests from scratch, deploy a second application alongside your nginx server, and inject configuration using a ConfigMap.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely on your local machine, so there are no ongoing cloud costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- A minikube Kubernetes cluster running as Docker containers (uses local disk and RAM).
- YAML manifest files (app-config.yaml and httpd-app.yaml).
- Docker Desktop (installed application and container runtime).
- minikube and kubectl (Homebrew packages).
Keep everything running
No action needed. Choose this if you are still actively building or plan to explore more Kubernetes commands.
- Your minikube cluster persists across terminal sessions. If your Mac restarts, resume the cluster by running minikube start.
- Docker Desktop continues running in the background. It uses some RAM and CPU while idle.
- Your YAML files remain wherever you saved them.
Pause - I'll come back to this later
Shut down running processes to free up memory and CPU, but keep everything installed so you can resume quickly.
- Stop the minikube cluster by running:
minikube stop
- This stops the Docker containers that run your cluster but preserves all state. Your Deployments, Services, and ConfigMaps remain intact.
- Quit Docker Desktop by clicking the Docker whale icon in the menu bar and selecting Quit Docker Desktop.
- When you are ready to resume, open Docker Desktop and run:
minikube start
- Everything will be back exactly as you left it.
Delete - I don't want to use this again
Remove all project resources and start fresh. This frees up all disk space and RAM used by the cluster.
- Delete the minikube cluster and free all resources by running:
minikube delete
- Remove the YAML manifest files by running:
rm app-config.yaml httpd-app.yaml
- Uninstall minikube and kubectl by running:
brew uninstall minikube
brew uninstall kubectl
- To uninstall Docker Desktop, open Docker Desktop, select the Troubleshoot icon in the top-right corner of the Dashboard, then select Uninstall. Confirm by selecting Uninstall again. You can then move the Docker application to the Trash.
That's a Wrap!
That's a Wrap!
Nice work! You just deployed, scaled, broke, and healed a real application on a local Kubernetes cluster. You now have hands-on experience with the same building blocks that power production workloads at companies worldwide.
You learned how to:
- Set up a local Kubernetes cluster with minikube, deployed a containerized nginx web server using a Deployment, and exposed it to the outside world using a Service.
- Witnessed Kubernetes self-healing by deleting Pods and watching them recreate automatically, then scaled your application from 1 to 5 replicas with a single command.
- Performed a zero-downtime rolling update to swap your container image and rolled it back safely with kubectl rollout undo.
- Secret Mission: Wrote YAML manifests from scratch, created a ConfigMap, and deployed a second application with injected environment variables.
Ready to quiz yourself?