Containerize and Deploy with Kubernetes
Build a Node.js app, containerize it with Docker, and deploy it to Kubernetes.
Introduction
30 Second Summary
When you visit a website, your request gets routed to one of many identical copies of an app running across a fleet of servers. If one copy crashes, another takes over before you even notice.
In this project, you will package a Node.js web server into a Docker container and deploy it to a local Kubernetes cluster. From there, you will scale your app, watch it self-heal, push a live update, and roll it back.
What You'll Build
You will refresh your browser and watch the hostname change as traffic bounces between pods, kill a pod and see Kubernetes replace it automatically, then push a v2 update and undo it with a single command.
By the end of this project, you'll have:
- A Docker image built from a Dockerfile you wrote, proving your app is containerized and portable.
- Your app running as a Kubernetes Deployment, load-balanced across pods by a Service you can access in your browser.
- Hands-on proof of scaling, self-healing, rolling updates, and rollbacks you triggered with kubectl.
- Secret Mission: Externalize your app's configuration with a ConfigMap and add resource limits to your pods.
Are there any prerequisites?
This project assumes you already have Docker installed on your Mac. You should also be comfortable with basic terminal commands like cd and ls.
Before We Start
Before we dive in, let's lock in what you're about to build and why it matters. This step is your chance to commit to the project goal before any hands-on work begins.
Set Up Minikube and kubectl
Your app will eventually run inside a Kubernetes cluster. Before you can deploy anything, you need a local cluster on your own machine to work with.
Minikube creates a single-node Kubernetes cluster right on your Mac. In this step, you'll install Minikube and kubectl (the command-line tool for managing any Kubernetes cluster), then start your local cluster.
In this step, get ready to:
- Install kubectl and Minikube.
- Start a local Kubernetes cluster.
- Verify kubectl can communicate with the cluster.
Install kubectl and Minikube
You need two tools: kubectl to send commands to any Kubernetes cluster, and minikube to create a local cluster on your Mac. Both install through Homebrew.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Terminal and press Enter to open it.
- Check if kubectl is already on your machine by running:
kubectl version --client
✔️ I see a version number
kubectl is already installed. Continue to the Minikube check below.
ⓧ Command not found
No worries. This means kubectl is not installed yet.
- Install kubectl by running:
brew install kubectl
- Verify the installation succeeded by running:
kubectl version --client
You should now see version information in the output.
Seeing 'brew: command not found'?
This means Homebrew is not installed on your Mac. Visit brew.sh and follow the one-line install command, then retry the brew install kubectl command.
Help me troubleshoot my kubectl installation.
- Check if Minikube is already on your machine by running:
minikube version
✔️ I see a version number
Minikube is already installed. Continue to the next substep below.
ⓧ Command not found
No worries. This means Minikube is not installed yet.
- Install Minikube by running:
brew install minikube
- Verify the installation succeeded by running:
minikube version
You should now see a version number in the output.
Install failing?
Make sure Homebrew is installed (see the kubectl tab above for instructions). If Homebrew is working but the install still fails, check your internet connection and retry.
Help me troubleshoot my Minikube installation.
Start a local Kubernetes cluster
With both tools installed, you can now create a Kubernetes cluster on your Mac. Minikube uses your existing Docker installation to run the cluster inside a container.
- Start your local cluster by running:
minikube start
What does this command do?
minikube start creates a single-node Kubernetes cluster on your machine. It downloads the Kubernetes components, sets up networking, and configures kubectl to point at the new cluster automatically.
This may take a minute or two the first time while it downloads everything it needs. You'll see progress messages in the terminal as each component starts.
- Confirm the cluster is running by running:
minikube status
You should see output showing host: Running, kubelet: Running, and apiserver: Running. This means all the cluster components are healthy.
Cluster not starting?
Make sure Docker Desktop is running. Minikube needs Docker to create the cluster. Open Docker Desktop and wait for the whale icon in your menu bar to stop animating before retrying.
If you see a resource error, check that your Mac has at least 2 CPUs and 2GB of free RAM available.
Help me troubleshoot my minikube start failure.
Verify kubectl can talk to the cluster
The cluster is running, but can kubectl actually communicate with it? Every Kubernetes cluster has nodes, the machines that run your containers. Listing them proves the connection works.
Before you run the next command, what do you expect to see? How many nodes should a single-node Minikube cluster have?
- List the nodes in your cluster by running:
kubectl get nodes
You should see one node named minikube with a STATUS of Ready. That confirms kubectl is connected and your cluster is ready to accept workloads.
Node not showing as Ready?
If you see NotReady, wait 30 seconds and run kubectl get nodes again. The node can take a moment to finish starting up.
If you see a connection error, run minikube status to check whether the cluster is still running. If it stopped, run minikube start again.
Help me fix my kubectl connection to Minikube.
Your local cluster is up and kubectl is ready to manage it. Next up, you'll build a Node.js app, package it into a Docker image, and run your first container.
Build and Run Your App with Docker
Your Kubernetes cluster is running and ready for workloads. Now you need an app to deploy to it.
Before Kubernetes enters the picture, you need your app packaged as a Docker image. Building and running it locally first proves the container works in isolation, so you can trust it before handing it to the cluster.
In this step, get ready to:
- Write a Node.js web server and a Dockerfile to containerize it.
- Build a Docker image from your Dockerfile.
- Run your app in a container and verify it responds in your browser.
Create your project files
You need two files to containerize an app: the application code itself, and a Dockerfile that tells Docker how to package it into an image.
- Move to your Desktop and create a project folder by running these commands:
cd ~/Desktop
mkdir hello-k8s
cd hello-k8s
- Create server.js by running this command, which writes the file contents in one step:
cat > server.js << 'EOF'
// Import Node.js built-in modules
const http = require("http");
const os = require("os");
// Set the port the server listens on
const port = 3000;
// Create an HTTP server that responds with JSON
const server = http.createServer((req, res) => {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(
JSON.stringify({
app: "hello-k8s",
version: "1.0",
// os.hostname() returns the container/pod name at runtime
hostname: os.hostname(),
})
);
});
// Start the server
server.listen(port, () => {
console.log(`Server running on port ${port}`);
});
EOF
What does this server do?
- http.createServer creates a basic web server using Node.js built-in modules. No npm install needed.
- Every request gets a JSON response with the app name, version, and the machine's hostname.
- os.hostname() is the key detail. Inside a container or pod, it returns the container/pod name instead of your Mac's name. Later, this is how you will see which pod handled each request.
- Confirm the file was created by running:
ls
You should see server.js listed in the output.
- Create the Dockerfile by running this command:
cat > Dockerfile << 'EOF'
# Use a lightweight Node.js base image
FROM node:22-alpine
# Set the working directory inside the container
WORKDIR /app
# Copy the server file into the container
COPY server.js .
# Document the port the app listens on
EXPOSE 3000
# Start the server when the container runs
CMD ["node", "server.js"]
EOF
What does this Dockerfile do?
- FROM node:22-alpine starts from a tiny Linux image that already has Node.js installed.
- WORKDIR /app sets the directory inside the container where your code lives.
- COPY server.js . copies your server file from your Mac into the container.
- CMD ["node", "server.js"] tells Docker what to run when the container starts.
- Confirm both files exist by running:
ls
You should see both Dockerfile and server.js in the output.
✔️ Awesome, I've got everything!
Great. Make sure both files are in the hello-k8s folder, then move on to the build.
ⓧ I'd like to double check the full code
Here is the full server.js file:
const http = require("http");
const os = require("os");
const port = 3000;
const server = http.createServer((req, res) => {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(
JSON.stringify({
app: "hello-k8s",
version: "1.0",
hostname: os.hostname(),
})
);
});
server.listen(port, () => {
console.log(`Server running on port ${port}`);
});
And here is the full Dockerfile:
FROM node:22-alpine
WORKDIR /app
COPY server.js .
EXPOSE 3000
CMD ["node", "server.js"]
Missing a file?
- Make sure you are inside the hello-k8s folder. Run pwd to check your current directory.
- If a file is missing, re-run the corresponding cat > filename << 'EOF' command from above. Make sure you copy the entire block, including the closing EOF line.
Why are my project files missing?
Build the Docker image
Docker reads your Dockerfile line by line and assembles an image. An image is a snapshot of everything your app needs to run: the operating system, Node.js, and your code.
- Build your Docker image by running this command:
docker build -t hello-k8s:v1 .
What does this command do?
- docker build reads your Dockerfile and assembles the image layer by layer.
- -t hello-k8s:v1 tags the image with the name hello-k8s and version v1. Tags let you track different versions of the same image.
- The . at the end tells Docker to look for the Dockerfile in the current directory.
- Verify the image was built by running:
docker images
You should see hello-k8s listed with the tag v1. The image size should be small thanks to the Alpine base.
Build failed?
- Make sure Docker Desktop is running. You can check by looking for the Docker whale icon in your menu bar.
- Confirm you are in the hello-k8s folder by running pwd. The Dockerfile must be in the same directory where you run the build command.
- If you see a file-not-found error for server.js, check that server.js is in the same folder as your Dockerfile. Run ls to confirm.
Help me debug my Docker build failure.
Run your container and verify
Your image is built. Now you can launch a container from it to prove the app actually works before deploying it to the cluster.
Before you run the container, think about what you expect to see when you visit http://localhost:3000 in your browser. What fields should the JSON response contain?
- Start a container from your image by running this command:
docker run -p 3000:3000 hello-k8s:v1
What does this command do?
- docker run creates and starts a new container from the hello-k8s:v1 image.
- -p 3000:3000 maps port 3000 on your Mac to port 3000 inside the container. Without this flag, the container's port is unreachable from your browser.
Your terminal now shows Server running on port 3000. The container is running in the foreground, so your terminal is occupied.
- Open your web browser and go to http://localhost:3000.
You should see a JSON response containing three fields: app set to hello-k8s, version set to 1.0, and hostname showing a random container ID. That hostname is the key value you will use later to tell which pod handled a request.
- Return to your terminal and press Ctrl+C to stop the container.
Don't see the JSON response?
- Check your terminal for error messages. If the container exited immediately, there may be a typo in server.js. Use the double-check tab above to compare your code.
- If you see a message about port 3000 already being in use, another process is occupying that port. Stop any other running containers or applications using port 3000, then try the docker run command again.
- Make sure you include the -p 3000:3000 flag. Without it, the container's port is not mapped to your Mac and the browser cannot reach it.
My Docker container is not responding on localhost:3000.
Your app is containerized and proven to work. Next, you will deploy this exact image to your Kubernetes cluster and let the cluster manage it for you.
Deploy to Kubernetes
In the last step, you built a Docker image and ran it as a container. It worked. But that container was running on your machine with nothing watching over it.
Kubernetes solves this with Deployments. Instead of manually running containers, you write a manifest that describes your desired state. Kubernetes continuously enforces that state, restarting anything that crashes. In this step, you'll deploy your app to the local cluster.
In this step, get ready to:
- Load your Docker image into Minikube's image store.
- Write a Kubernetes Deployment manifest.
- Apply the manifest and confirm a pod is running.
Load your Docker image into Minikube
Your Docker image lives in your local Docker image store. Minikube runs its own separate environment with its own image store. Before the cluster can run your app, you need to explicitly copy the image over.
- Load the hello-k8s:v1 image into Minikube by running this command:
minikube image load hello-k8s:v1
This command produces no output on success. It may take a few seconds to transfer the image. When your terminal prompt returns, the image is loaded.
Command hanging or erroring?
- Make sure your Minikube cluster is running. Run minikube status and confirm all components show as Running.
- Make sure the image name matches exactly. Run docker images and confirm you see hello-k8s with tag v1.
- help me troubleshoot minikube image load
Write the Deployment manifest
A Deployment manifest is a YAML file that tells Kubernetes what app to run, which image to use, and how many copies to keep alive. Kubernetes reads this file and creates the resources you described.
- In the same folder where server.js and Dockerfile live, create a new file called deployment.yaml and add the following content:
apiVersion: apps/v1
kind: Deployment
metadata:
name: hello-k8s
spec:
replicas: 1
selector:
matchLabels:
app: hello-k8s
template:
metadata:
labels:
app: hello-k8s
spec:
containers:
- name: hello-k8s
image: hello-k8s:v1
imagePullPolicy: Never
ports:
- containerPort: 3000
What does this manifest do?
- apiVersion: apps/v1 and kind: Deployment tell Kubernetes you want to create a Deployment resource.
- replicas: 1 tells Kubernetes to run exactly one copy of your app. Kubernetes will maintain this count, restarting the Pod if it ever crashes.
- selector.matchLabels tells the Deployment which Pods belong to it. The labels in the template section must match this selector.
- imagePullPolicy: Never tells Kubernetes to use the image you loaded into Minikube instead of trying to pull from a remote registry.
- containerPort: 3000 declares the port your Node.js server listens on inside the container.
- Save deployment.yaml.
YAML looking messy?
- YAML is whitespace-sensitive. Make sure you are using spaces, not tabs. Each indentation level is exactly two spaces.
- Check that containers: is indented under spec: (the inner one, inside template), not under the outer spec:.
- help me fix my deployment.yaml indentation
✔️ Awesome, I've got everything!
Great. Make sure you have saved deployment.yaml before moving on.
ⓧ I'd like to double check the full code
Here is the complete deployment.yaml for comparison:
apiVersion: apps/v1
kind: Deployment
metadata:
name: hello-k8s
spec:
replicas: 1
selector:
matchLabels:
app: hello-k8s
template:
metadata:
labels:
app: hello-k8s
spec:
containers:
- name: hello-k8s
image: hello-k8s:v1
imagePullPolicy: Never
ports:
- containerPort: 3000
Apply the manifest and check your pod
Your manifest describes what you want. Now hand it to Kubernetes and let the cluster bring it to life.
- Apply the Deployment to your cluster by running this command:
kubectl apply -f deployment.yaml
You should see deployment.apps/hello-k8s created confirming Kubernetes accepted your manifest.
Before you check the result, what do you think kubectl get pods will show? How many pods, and what status?
- Check the pod Kubernetes created by running:
kubectl get pods
You should see one pod with a name starting with hello-k8s- and a STATUS of Running. That means Kubernetes pulled your image, created a Pod, and your app is alive inside the cluster.
What is a pod?
A Pod is the smallest deployable unit in Kubernetes. It wraps one or more containers and gives them a shared network and storage context. Your Deployment created one Pod, and that Pod is running your hello-k8s:v1 container.
- Copy the full pod name from the output above (it looks like hello-k8s-xxxxxxxxxx-xxxxx).
- View the pod's logs by running:
kubectl logs [[POD_NAME="your pod name from kubectl get pods"]]
You should see Server running on port 3000 in the output. That is your Node.js server running inside the cluster.
Pod not showing as Running?
- If you see ImagePullBackOff or ErrImageNeverPull, the image was not loaded into Minikube. Run minikube image load hello-k8s:v1 again, then delete the pod with kubectl delete pod followed by the pod name. The Deployment will create a new one automatically.
- If you see CrashLoopBackOff, the container is crashing. Run kubectl logs followed by the pod name to see the error. Check that your server.js matches the code from Step 2.
- help me fix my Kubernetes pod that is not running
Your app is alive inside the cluster. But try reaching it from your browser and nothing happens. Next up, you'll create a Service to open a door from the outside world into your cluster.
Expose Your App with a Service
Your app is running inside the Kubernetes cluster. You confirmed it with kubectl get pods and kubectl logs in the last step. But can you actually reach it from your browser?
Pods get internal cluster IPs that are invisible to your machine. In this step, you will see this problem firsthand, then fix it with a Kubernetes Service that provides a stable endpoint to route traffic to your pods.
In this step, get ready to:
- Discover why pods are unreachable from outside the cluster.
- Create a Service manifest that routes external traffic to your pod.
- Access your running app in the browser through the Service.
Try to reach your pod from outside the cluster
Every pod in Kubernetes gets its own IP address inside the cluster's virtual network. Let's find yours and see if you can reach it from your terminal.
- View your pod's IP address by running this command:
kubectl get pods -o wide
You will see extra columns in the output. Look for the IP column, which shows your pod's cluster-internal IP address (something like 10.244.0.5).
Before you try the next command, do you think you can reach your app by curling this IP from your terminal?
- Try to reach your pod directly by running this command:
curl --connect-timeout 5 http://[[POD_IP="your pod's IP address"]]:3000
After a few seconds, the command fails. The request never reaches your pod.
Why can't I reach the pod's IP?
Kubernetes assigns each pod an IP from the cluster's internal network. These IPs are only routable between pods and nodes inside the cluster.
Your Mac sits outside that network, so the request has nowhere to go. To make your app accessible from outside, you need a Kubernetes Service that bridges the gap between the cluster's internal network and your machine.
Create a Service manifest
A Kubernetes Service is a resource that sits in front of your pods and routes traffic to them. You define it in a YAML manifest, just like your Deployment.
- In your project directory, create a new file called service.yaml with the following content:
# Defines a Service to expose pods externally
apiVersion: v1
kind: Service
metadata:
name: hello-k8s-service
spec:
# NodePort opens a port on the cluster node
type: NodePort
# Routes traffic to pods matching this label
selector:
app: hello-k8s
ports:
# Service listens on port 80, forwards to container port 3000
- port: 80
targetPort: 3000
What does this manifest do?
- type: NodePort opens a port on every cluster node, making the Service reachable from outside the cluster. Minikube's minikube service command uses this port to give you direct browser access.
- selector: app: hello-k8s tells the Service to route traffic to any pod carrying the app: hello-k8s label. This matches the label in your deployment.yaml.
- port: 80 is the port the Service listens on. targetPort: 3000 forwards traffic to the port your Node.js server listens on inside the container.
Why NodePort instead of LoadBalancer?
Kubernetes offers several Service types. NodePort is the simplest for local development. It opens a port directly on the cluster node and routes traffic to your pods.
A LoadBalancer type would require running minikube tunnel in a separate terminal, adding complexity. NodePort combined with minikube service gives you browser access in a single command.
- Save service.yaml.
- Confirm the file exists by running:
ls
You should see service.yaml listed alongside server.js, Dockerfile, and deployment.yaml.
Don't see service.yaml?
Make sure you saved the file in the same directory as your other project files. Run pwd to check which directory you are in.
help me find where my service.yaml file was saved
✔️ Awesome, I've got everything!
Great. Make sure you have saved service.yaml before continuing.
ⓧ I'd like to double check the full code
Here is the complete service.yaml file. Compare it with yours:
apiVersion: v1
kind: Service
metadata:
name: hello-k8s-service
spec:
type: NodePort
selector:
app: hello-k8s
ports:
- port: 80
targetPort: 3000
Apply the Service and access your app
Your manifest is ready. Now you will apply it to the cluster and finally reach your app from outside.
- Apply the Service manifest to your cluster by running:
kubectl apply -f service.yaml
What does this command do?
kubectl apply -f service.yaml sends your Service definition to the Kubernetes API server. Kubernetes creates the Service and begins routing traffic from the NodePort to any pod matching the app: hello-k8s selector.
- Confirm the Service was created by running:
kubectl get service
You should see hello-k8s-service listed with type NodePort.
Don't see hello-k8s-service?
Make sure you ran kubectl apply -f service.yaml and saw a confirmation message. If you see an error, double check your service.yaml against the double-check tab above.
help me fix my Kubernetes Service that isn't showing up
Now for the moment of truth. Earlier, curling the pod's IP directly failed. Before you run the next command, what do you think will happen this time?
- Open your app in the browser through the Service by running:
minikube service hello-k8s-service
Your browser opens and displays the JSON response from your app. You should see three fields: app with the value hello-k8s, version with 1.0, and hostname showing a long string.
That hostname value is the actual name of the pod serving your request. Kubernetes assigned it automatically when the pod was created.
Browser not opening?
Make sure your Minikube cluster is still running by running minikube status. All components should show as Running.
Also confirm your pod is still healthy by running kubectl get pods. The pod should show STATUS: Running.
help me troubleshoot why minikube service is not opening my browser
Your app is live and reachable from outside the cluster. Next up, you will scale to multiple replicas, watch Kubernetes self-heal, and perform a rolling update.
Scale, Self-Heal, and Rolling Updates
Your app is live in the browser, routed through a Kubernetes Service. Right now, a single pod handles every request.
That single pod is a single point of failure. If it crashes, your app goes down and nobody restarts it. In this step, you will scale to multiple replicas, deliberately kill a pod to see self-healing in action, push a v2 update with zero downtime, and roll it all back.
In this step, get ready to:
- Scale your deployment to 3 replicas and observe load balancing and self-healing.
- Perform a rolling update from v1 to v2 of your app.
- Roll back to v1 with a single command.
Scale to 3 replicas and observe self-healing
Right now, one pod handles everything. Scaling adds more pods, and Kubernetes distributes traffic across them automatically through the Service you already created.
- Scale your deployment to 3 replicas by running this command:
kubectl scale deployment/hello-k8s --replicas=3
- Confirm all 3 pods are running by running this command:
kubectl get pods
You should see 3 pods listed, each with STATUS Running. Each pod has its own unique name.
What just happened?
The kubectl scale command told Kubernetes you want 3 copies of your app. Kubernetes created 2 additional pods to match that desired state.
The Service automatically discovers all pods with the app: hello-k8s label. No reconfiguration needed.
- Switch back to the browser tab showing your app's JSON response.
- Refresh the page 5 or 6 times.
Each time you refresh, the hostname value in the JSON response changes. The Service is distributing your requests across all 3 pods.
Now for the real test. What happens if a pod dies?
Before you try this, what do you think Kubernetes will do if you delete one of your 3 pods?
- Copy any one of the pod names from the kubectl get pods output you ran a moment ago.
- Delete that pod by running this command:
kubectl delete pod [[PODNAME="one of the pod names from kubectl get pods"]]
- Immediately check the pods again by running:
kubectl get pods
You still see 3 pods. One has a much younger AGE than the others. Kubernetes detected the missing pod and instantly created a replacement to maintain the desired 3 replicas. This is self-healing.
Still seeing only 2 pods?
- Run kubectl get pods again after a few seconds. Kubernetes may take a moment to schedule the replacement pod.
- Make sure you see 3 rows in the output. If a pod shows STATUS ContainerCreating, it is still starting up. Wait a few seconds and recheck.
- Why isn't Kubernetes replacing my deleted pod?
Perform a rolling update to v2
Kubernetes can update your app without any downtime. You push a new container image, and Kubernetes gradually replaces old pods with new ones. This is called a rolling update.
- In server.js, find this line:
version: "1.0",
- Change "1.0" to "2.0", so the line looks like this:
version: "2.0",
- Save server.js.
- Build the updated image as hello-k8s:v2 by running this command:
docker build -t hello-k8s:v2 .
You should see each build step complete. Most layers will be cached from your v1 build, so this finishes quickly.
- Load the new image into Minikube by running:
minikube image load hello-k8s:v2
- Trigger the rolling update by running this command:
kubectl set image deployment/hello-k8s hello-k8s=hello-k8s:v2
What does this command do?
This tells Kubernetes to update the container named hello-k8s inside the hello-k8s Deployment to use the hello-k8s:v2 image. The first hello-k8s after deployment/ is the Deployment name. The second is the container name from your YAML manifest.
- Watch the rollout progress by running:
kubectl rollout status deployment/hello-k8s
You should see a message confirming the deployment rolled out successfully. Kubernetes replaced the old v1 pods with new v2 pods one at a time, keeping your app available throughout.
- Refresh your browser tab.
The response now shows "version": "2.0". Your app was updated with zero downtime.
How does a rolling update work?
Kubernetes creates a new pod running v2 before terminating an old v1 pod. It repeats this process until all replicas are running v2.
At no point are all pods down at the same time. Your app stays available to users during the entire update.
Still seeing version 1.0 in the browser?
- Make sure you ran minikube image load hello-k8s:v2 before triggering the update. Without this, the cluster cannot find the v2 image.
- Run kubectl get pods and check that all pods show a recent AGE. If older pods remain, the rollout may still be in progress.
- Check that you changed "1.0" to "2.0" in server.js and saved the file before running docker build.
- Why isn't my rolling update showing version 2.0?
Roll back to v1
Sometimes an update introduces a bug. Kubernetes keeps a history of your deployments, so you can roll back to the previous version with one command.
- Roll back your deployment to the previous version by running:
kubectl rollout undo deployment/hello-k8s
- Confirm the rollback completed by running:
kubectl rollout status deployment/hello-k8s
You should see a message confirming the deployment rolled out successfully.
Before you check your browser, what version do you think it will show now?
- Confirm all 3 pods are still running by running:
kubectl get pods
You should see 3 pods with STATUS Running.
- Refresh your browser tab.
The response shows "version": "1.0" again. Kubernetes rolled every pod back to the v1 image with a single command.
Browser still showing version 2.0?
- Run kubectl rollout status deployment/hello-k8s and confirm the rollout completed. The old v2 pods may still be terminating.
- Wait a few seconds and refresh again. Kubernetes needs a moment to terminate the v2 pods and start v1 replacements.
- Why is my rollback not showing version 1.0 in the browser?
The rollback reverted the pods running inside Kubernetes, but server.js on your machine still says "2.0". Let's sync the file back to match.
- In server.js, find this line:
version: "2.0",
- Change "2.0" back to "1.0", so the line looks like this:
version: "1.0",
- Save server.js.
Why revert server.js on disk?
The kubectl rollout undo command rolled back the container images running inside Kubernetes. It does not touch the files on your machine.
Reverting server.js keeps your source code in sync with what the cluster is actually running. This avoids confusion if you build a new image later.
Secret mission
ConfigMaps and Resource Limits
Your app's configuration is baked directly into the container image. If you need to change a single greeting or toggle a feature flag, you have to rebuild the entire image and redeploy. In this mission, you will externalize configuration with a Kubernetes ConfigMap and add CPU and memory resource limits to protect your cluster from runaway pods.
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 costs.
Resources you used:
- A local Minikube cluster running as a Docker container on your Mac, containing your Deployment, Service, and ConfigMap.
- Docker images: hello-k8s:v1, hello-k8s:v2, and hello-k8s:v3.
- Project files on your Desktop: server.js, Dockerfile, deployment.yaml, service.yaml, and configmap.yaml.
- Minikube and kubectl CLI tools installed via Homebrew.
Keep everything running
No action needed. Choose this if you are still actively experimenting with your cluster or want to try more Kubernetes commands.
- Your Minikube cluster stays running in the background. It uses some CPU and memory on your Mac while active.
- All Kubernetes resources (Deployment, Service, ConfigMap) remain deployed and accessible via minikube service hello-k8s-service.
- Your Docker images and project files stay on disk.
Pause - I'll come back to this later
Shut down the cluster to free up CPU and memory. Your Kubernetes resources are preserved and come back when you start the cluster again.
- Stop the Minikube cluster by running:
minikube stop
- When you are ready to resume, run minikube start to bring the cluster back with all your resources intact.
- Your Docker images and project files remain untouched on disk.
Delete - I don't want to use this again
Remove all project resources and start fresh. This deletes the cluster, images, project files, and CLI tools.
- Delete the Minikube cluster by running:
minikube delete
This removes the cluster and all Kubernetes resources inside it (your Deployment, Service, and ConfigMap).
- Remove the Docker images by running:
docker rmi hello-k8s:v1 hello-k8s:v2 hello-k8s:v3
- Delete your project folder from the Desktop by dragging it to the Trash, or by running rm -rf on it in your terminal.
- Uninstall the CLI tools by running:
brew uninstall minikube kubectl
- Optionally, remove leftover configuration directories by running:
rm -rf ~/.minikube ~/.kube
Nice Work!
Nice Work!
Nice work! You just containerized a Node.js app with Docker, deployed it to a local Kubernetes cluster, and operated it like a real production service.
You've learned how to:
- Built a Docker image from scratch using a Dockerfile and the node:22-alpine base image, then deployed it to a local Kubernetes cluster using declarative YAML manifests.
- Exposed your app to the outside world with a Kubernetes Service, scaled to multiple replicas with load balancing, and watched Kubernetes self-heal by automatically replacing a deleted pod.
- Performed a zero-downtime rolling update to v2 of your app and rolled it back to v1 with a single command.
- Secret Mission: Externalized configuration with a ConfigMap and applied resource limits to your pods.
Ready to quiz yourself?