Build a Self-Healing Kubernetes App
Build a Dockerized web app that Kubernetes scales, heals, and updates locally.
Introduction
30 Second Summary
A web app can vanish the moment its only running copy stops. When that copy disappears, visitors have no page to load.
In this project, you will package a live status page with Docker. You will run the same page as a self-healing Kubernetes workload that maintains three available copies.
What You'll Build
Picture deleting the Pod named on http://localhost:8081, then refreshing the page to see the same app return with a different Pod identity.
By the end of this project, you'll have:
- A live Version 1 page that displays docker-container when Docker serves it. The identity changes to a generated Pod name after Kubernetes takes over.
- A reusable container image tagged self-healing-web:v1 that you can run locally. Stopping its only container gives you visible proof of an unmanaged outage.
- A three-replica Deployment that Kubernetes maintains at the desired size. Deleting the Pod shown in your browser triggers a replacement with a different name.
- Secret Mission: Build Version 2 and trigger a controlled rolling update across three replicas.
Are there any prerequisites?
You'll need a Mac with Docker Desktop version 4.75.0 or newer, Visual Studio Code, and internet access for the initial downloads. No cloud account or Docker Hub account is required.
Before We Start
This checkpoint turns the project goal into a clear mental model before any hands-on work begins. It frames the contrast between one unmanaged Docker container and a self-healing Kubernetes Deployment.
Verify Your Local Workspace
Your status page eventually moves from a local Docker image into a Kubernetes cluster. That handoff depends on Docker Desktop using a compatible version with the correct image store.
This workspace check catches hidden engine or storage mismatches before you build anything. You will also prepare an empty folder in Visual Studio Code for the project files.
In this step, get ready to:
- Confirm that Docker Desktop meets the required version.
- Enable the containerd image store.
- Create the project folder and verify the Docker engine.
Check your Docker Desktop version
Docker Desktop must be version 4.75.0 or newer. This version floor avoids a local-image issue that could block the Kubernetes cluster later.
- Press Cmd+Space to open Spotlight on your Mac.
- Type Docker Desktop into the search field.
- Press Enter to start Docker Desktop.
- Wait until Docker Desktop finishes starting.
- Select the Docker menu in the macOS menu bar.
- Select About Docker Desktop to find the installed version.
✔️ I see version 4.75.0 or newer
Docker Desktop meets the project requirement. Keep it running while you continue.
ⓧ I see an older version
Docker Desktop needs an update before the project can use local images with the integrated kind cluster. The update can take several minutes while the application replaces its current version.
- Return to the Docker menu.
- Select Check for updates.
- Install the available update.
- Reopen About Docker Desktop after the update finishes.
- Confirm that the version is now 4.75.0 or newer.
Update still unavailable?
Make sure your Mac has an internet connection before checking again. Your organisation may manage Docker Desktop updates if this is a work device.
Help me update Docker Desktop on my Mac.
ⓧ Docker Desktop does not open
Docker Desktop must finish starting before its engine can answer commands. A stalled launch usually points to a local application issue.
- Restart your Mac.
- Open Docker Desktop again through Spotlight.
- Wait until the Docker menu becomes available.
Docker Desktop still not starting?
Check whether macOS is showing a permission request behind another window. Approve the request before reopening Docker Desktop.
Help me start Docker Desktop on my Mac.
That clears the first compatibility check. Docker Desktop is now current enough for the local cluster you will create later.
Enable the image store and create your folder
Docker Desktop keeps its classic store separate from the containerd image store. The integrated kind provisioner needs the containerd store to use the image you build locally.
- Select the Docker menu in the macOS menu bar.
- Select Settings.
- Select General in the Settings sidebar.
- Enable Use containerd for pulling and storing images if the checkbox is clear.
- Select Apply if you changed the checkbox.
Why the containerd image store?
Docker Desktop maintains separate classic and containerd image stores. An image in the inactive store stays on disk but remains hidden until you switch back.
Keeping containerd enabled gives the Docker build and the future kind cluster access to the same local image workflow.
The checkbox should remain selected. This confirms that Docker Desktop uses the image store required by the project.
Your project also needs one named workspace where the page template and container instructions can live. Creating it through VS Code opens the folder as the active workspace.
- Press Cmd+Space to open Spotlight.
- Type Visual Studio Code into the search field.
- Press Enter to open Visual Studio Code.
- Select File from the menu bar.
- Select Open Folder....
The folder picker now controls where the project lives. You will keep it on your Desktop so it stays easy to find.
- Select Desktop in the folder picker.
- Select New Folder.
- Enter self-healing-web as the folder name.
- Confirm the new folder.
- Select the new self-healing-web folder.
- Select Open.
- Select Yes, I trust the authors if VS Code displays a Workspace Trust prompt.
You should see self-healing-web at the top of the Explorer sidebar. The folder is empty because the project files belong in the next step.
Verify the Docker client and server
The Docker CLI sends commands from your terminal to the Docker Engine. Seeing both sides in one response proves that the command-line tool can reach the running engine.
- Select View from the VS Code menu bar.
- Select Terminal to open the integrated terminal inside self-healing-web.
Before you run the check, do you expect one command to report both halves of Docker?
- Confirm that the Docker client can reach the server by running this command:
docker version
What does this command show?
- The Client section reports the Docker command-line components installed on your Mac.
- The Server section reports the running Docker engine that builds images and starts containers.
You should see separate Client and Server sections in the terminal output. That result confirms the local Docker workspace is ready.
Missing the Server section?
Make sure Docker Desktop is still running. Wait for it to finish starting before rerunning the command.
If the terminal cannot find the Docker command, close VS Code after Docker Desktop starts. Reopen the self-healing-web folder before trying again.
Help me verify my Docker client and server.
Your Docker environment can now build and run local images from the correct store. Next, you will package the status page as its first versioned container image.
Package and Run the Status Page
Your local Docker workspace is ready. The browser still has nothing it can load from this project.
Before orchestration can help, you need a real image. You also need a visible container that you can inspect.
In this step, get ready to:
- Create a status page with a runtime identity placeholder.
- Define a versioned NGINX image for the page.
- Build the image and run it as a local container.
Create the status page template
The NGINX Docker Official Image can turn ${POD_NAME} into runtime text when the container starts. The template stays reusable because each runtime supplies its own identity.
- In the VS Code Explorer sidebar, use the new-file control to create index.html.template inside the self-healing-web folder.
- Add the page structure by pasting this code:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Self-Healing Web App</title>
<style>
</style>
</head>
<body>
<main>
<span class="badge">Version 1</span>
<h1>Self-Healing Web App</h1>
<p>This request was served by:</p>
<code>${POD_NAME}</code>
<p class="hint">Package once with Docker. Run, scale, and recover it with Kubernetes.</p>
</main>
</body>
</html>
What does this structure do?
- The badge identifies this page as Version 1.
- The ${POD_NAME} placeholder reserves a visible location for the runtime identity.
- The empty <style> section gives the next two code chunks a safe insertion point.
- Save index.html.template.
You should see index.html.template listed in the VS Code Explorer sidebar.
- In index.html.template, place your cursor between <style> and </style>.
- Add the page layout styles by pasting this code:
:root {
font-family: system-ui, sans-serif;
color-scheme: dark;
}
body {
min-height: 100vh;
margin: 0;
display: grid;
place-items: center;
background: linear-gradient(135deg, #0f172a, #111827);
color: #e2e8f0;
}
main {
width: min(36rem, calc(100% - 3rem));
padding: 2rem;
border: 1px solid #334155;
border-radius: 1rem;
background: #111827;
box-shadow: 0 1.5rem 4rem #02061799;
}
How does the layout work?
- The :root selector gives the page a system font with a dark colour scheme.
- The body selector centres the status card against a dark background.
- The main selector creates the bordered card that holds the status details.
- Save index.html.template.
The <style> section should now contain complete rules for :root, body, and main.
- Place your cursor after the closing brace for main.
- Complete the visual styles by pasting this code:
.badge {
display: inline-block;
padding: 0.35rem 0.7rem;
border-radius: 999px;
background: #14532d;
color: #bbf7d0;
font-weight: 700;
}
h1 {
margin-bottom: 0.5rem;
}
code {
display: block;
margin: 1rem 0;
padding: 0.9rem;
border-radius: 0.5rem;
background: #020617;
color: #7dd3fc;
overflow-wrap: anywhere;
}
.hint {
color: #94a3b8;
line-height: 1.6;
}
What do these styles add?
- The .badge selector turns the version label into a green pill.
- The code selector gives the runtime identity its own readable panel.
- The .hint selector softens the supporting message beneath the identity.
- Save index.html.template.
Your template now contains the complete status page. The ${POD_NAME} placeholder remains available for NGINX to replace at startup.
Does the template structure look different?
Check that both CSS chunks sit between <style> and </style>. Make sure the page content remains inside <main>.
Keep ${POD_NAME} exactly as shown so the runtime can replace it.
Help me check my status page template.
✔️ Awesome, I've got everything!
Great. Double check that you saved index.html.template before moving on.
ⓧ I'd like to double check the full code
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Self-Healing Web App</title>
<style>
:root {
font-family: system-ui, sans-serif;
color-scheme: dark;
}
body {
min-height: 100vh;
margin: 0;
display: grid;
place-items: center;
background: linear-gradient(135deg, #0f172a, #111827);
color: #e2e8f0;
}
main {
width: min(36rem, calc(100% - 3rem));
padding: 2rem;
border: 1px solid #334155;
border-radius: 1rem;
background: #111827;
box-shadow: 0 1.5rem 4rem #02061799;
}
.badge {
display: inline-block;
padding: 0.35rem 0.7rem;
border-radius: 999px;
background: #14532d;
color: #bbf7d0;
font-weight: 700;
}
h1 {
margin-bottom: 0.5rem;
}
code {
display: block;
margin: 1rem 0;
padding: 0.9rem;
border-radius: 0.5rem;
background: #020617;
color: #7dd3fc;
overflow-wrap: anywhere;
}
.hint {
color: #94a3b8;
line-height: 1.6;
}
</style>
</head>
<body>
<main>
<span class="badge">Version 1</span>
<h1>Self-Healing Web App</h1>
<p>This request was served by:</p>
<code>${POD_NAME}</code>
<p class="hint">Package once with Docker. Run, scale, and recover it with Kubernetes.</p>
</main>
</body>
</html>
Define the container image
A Dockerfile is the recipe Docker uses to assemble an image. This recipe starts with the pinned NGINX base image before placing your template in its startup path.
- In the VS Code Explorer sidebar, use the new-file control to create Dockerfile inside the self-healing-web folder.
- Define the image by pasting this code:
FROM nginx:1.30.4-alpine
COPY index.html.template /etc/nginx/templates/index.html.template
ENV NGINX_ENVSUBST_OUTPUT_DIR=/usr/share/nginx/html
What does this Dockerfile do?
- The FROM instruction uses the pinned nginx:1.30.4-alpine image as the foundation.
- The COPY instruction places your template where the NGINX startup process reads template files.
- The ENV instruction directs the rendered page into the NGINX static content directory.
- Save Dockerfile.
The VS Code Explorer sidebar should now list Dockerfile beside index.html.template.
Does Dockerfile have an extension?
Make sure the filename is exactly Dockerfile. Remove any extension that VS Code or macOS added.
Keep both files in the same self-healing-web folder so the copy instruction can find the template.
Help me check my Dockerfile and file locations.
✔️ Awesome, I've got everything!
Your two project files are ready. Keep both saved in the self-healing-web folder.
ⓧ I'd like to double check the full code
FROM nginx:1.30.4-alpine
COPY index.html.template /etc/nginx/templates/index.html.template
ENV NGINX_ENVSUBST_OUTPUT_DIR=/usr/share/nginx/html
Build and run the container
Building captures the page and its server configuration inside one versioned image. Running that image creates the live process that answers browser requests.
- Build the local image from the current self-healing-web folder by running this command in the integrated terminal:
docker build -t self-healing-web:v1 .
What does this command do?
- The docker build command asks Docker to assemble an image from the Dockerfile.
- The -t flag assigns the reusable tag self-healing-web:v1.
- The final . gives Docker access to the files in the current folder.
The first build may take a little longer while Docker downloads the NGINX base image. You should see the build finish without errors.
Did the image build fail?
Confirm the integrated terminal is using the self-healing-web folder. Check that Dockerfile has no filename extension.
Make sure Docker Desktop is still running if the terminal cannot reach the Docker Server.
Help me troubleshoot my Docker image build.
- Start the named container with its runtime identity by running this command:
docker run --name self-healing-web -d -p 127.0.0.1:8080:80 --env POD_NAME=docker-container self-healing-web:v1
How does the container start?
- The --name flag assigns the container the name self-healing-web.
- The -d flag keeps the container running in the background.
- The -p flag publishes local port 8080 to NGINX on container port 80.
- The --env flag supplies POD_NAME=docker-container for the template to render.
Before you open the page, which two details do you expect the status card to show?
- Open http://localhost:8080 in your browser.
You should see a dark status card showing Version 1. The runtime identity should read docker-container.
Page not loading?
Check that Docker Desktop is still running. Confirm that the run command completed without an error.
Make sure your browser address uses port 8080. Another local port does not reach this container.
Help me get the status page running.
You have a versioned image serving its runtime identity from a live container. Next, you'll test what happens when that single container stops.
Break the Single-Container Version
Your Version 1 status page is live. One Docker container currently serves every request.
A standalone container has no controller maintaining desired state. In this step, you'll stop it to test whether anything restores the app.
In this step, get ready to:
- Confirm the current page is served by the named container.
- Stop the standalone container to expose its missing desired-state controller.
- Remove the stopped container while preserving the versioned build.
Inspect the live container
The page identity connects a browser request to the runtime serving it. Checking that connection first gives you a clear baseline for the outage test.
- Switch back to the browser tab showing http://localhost:8080.
- Refresh the page.
You should see Version 1. The runtime identity should read docker-container.
- Return to the integrated terminal in VS Code.
- List the running containers by running this command:
docker ps
What Does the Container List Prove?
- The command lists containers that are currently running.
- The self-healing-web row connects the browser page to the named runtime you created.
You should see self-healing-web in the output. This confirms the container is running before you test its limits.
Don't See the Running Container?
If the page or row is missing, use Docker Desktop to start the existing self-healing-web container. Repeat both checks before continuing.
Help me restore the container before the outage test.
Test the single-container limit
Stopping the only runtime creates a direct test of desired state. You can now see whether another process restores the page.
Before you run this, do you think the page will keep responding after its only container stops?
- Change the container's running state by running this command:
docker stop self-healing-web
What Does the Stop Command Do?
The stop command asks Docker to stop the runtime named self-healing-web cleanly.
The reusable image remains stored locally.
- Refresh the browser tab at http://localhost:8080.
- Return to the integrated terminal in VS Code.
- Check the running-container list by running this command:
docker ps
What Does This Check Show?
The command reports the containers that remain active. A stopped container is absent from this running-only view.
This Outage Is Intentional
Your browser cannot load the status page. The only process serving it has stopped.
The running-container list no longer includes self-healing-web.
Nothing recreates the stopped runtime. You have now exposed the limit of an unmanaged container.
Does the Page Still Load?
Confirm that you refreshed http://localhost:8080. A different local address may be showing another application.
If the running-container list still includes self-healing-web, review the stop command output for an error.
Help me confirm the container outage.
Preserve the reusable image
The stopped container is a runtime record that you no longer need. Its image remains the reusable package for the next stage.
- Remove the stopped runtime by running this command:
docker rm self-healing-web
What Does the Remove Command Do?
The removal command deletes the stopped container record. The local self-healing-web:v1 image remains available.
Neither Dockerfile nor index.html.template changes during this test.
That cleanup completes the experiment. You still have the packaged Version 1 app ready for reuse.
Before the final check, do you expect the page or running-container list to recover on its own?
- Refresh the browser tab at http://localhost:8080.
The page should remain unavailable. No container is publishing the local port.
- Check the running-container list one final time by running this command:
docker ps
What Does the Final Check Prove?
The command lists active containers. The removed self-healing-web runtime cannot appear in this list.
You should not see self-healing-web in the output. The unavailable page confirms that nothing publishes 127.0.0.1:8080.
Still See the Container?
Review the earlier stop and removal outputs for an error. Resolve that error before repeating the final check.
Help me remove the standalone container safely.
You have proven the exact reliability gap the project needs to solve. Next, you'll move the same image into Kubernetes so a controller can maintain the app's desired state.
Move the App into Kubernetes
Your standalone Docker container is gone. The status page at http://localhost:8080 has no process keeping it available.
Kubernetes adds the missing orchestration layer. A Deployment maintains two replicas while a Service provides one stable access point.
In this step, get ready to:
- Create a one-node local Kubernetes cluster.
- Declare a two-replica Deployment with a ClusterIP Service.
- Open Version 1 through a Service port-forward.
Create the local Kubernetes cluster
Docker Desktop can create a local cluster that shares access to your local container images. This lets the cluster run self-healing-web:v1 without an external image registry.
Why Docker Desktop Kubernetes?
The integrated kind provisioner keeps the cluster local. It also avoids adding a separate cluster manager to this project.
The cluster uses Docker Desktop's containerd image store. Your existing local image can move directly into the orchestration workflow.
- Switch back to Docker Desktop.
- Select the Kubernetes view.
- Select Create cluster.
- Choose kind as the provisioner.
- Set the node count to 1.
- Confirm the cluster settings to start creation.
Cluster creation can take a few minutes while Docker Desktop downloads the required images. The Kubernetes view shows the cluster as running when it is ready.
That is the foundation in place. Docker Desktop has also installed kubectl so you can manage the cluster from your terminal.
- Return to the integrated terminal in VS Code.
- Switch kubectl to the Docker Desktop context by running:
kubectl config use-context docker-desktop
What does this command do?
A kubectl context identifies the cluster that receives your commands. This selects the local cluster managed by Docker Desktop.
Your terminal confirms that the Docker Desktop context is active.
Before you check the cluster, do you expect its node to be ready yet?
- Check the cluster node by running:
kubectl get nodes
What does this command show?
This asks the active cluster for its nodes. The status tells you whether the node can accept workloads.
You should see one node with the status Ready. This proves the cluster is available.
Node not ready?
Return to Docker Desktop and confirm that the Kubernetes cluster is running. Wait for cluster creation to finish before checking the node again.
If the command cannot reach a cluster, rerun the context command after Docker Desktop finishes creating it.
Help me troubleshoot my Docker Desktop Kubernetes cluster.
Declare the Deployment and Service
A Kubernetes manifest records your desired state in YAML. The controller uses that declaration to maintain the requested number of Pods.
The Downward API exposes each Pod's generated name through POD_NAME. The NGINX template renders that identity on the page.
- Create kubernetes.yaml inside the self-healing-web folder using VS Code's Explorer sidebar.
You should see kubernetes.yaml listed beside Dockerfile and index.html.template.
- Define the initial two-replica Deployment in kubernetes.yaml by pasting this code:
apiVersion: apps/v1
kind: Deployment
metadata:
name: self-healing-web
labels:
app: self-healing-web
spec:
replicas: 2
selector:
matchLabels:
app: self-healing-web
template:
metadata:
labels:
app: self-healing-web
spec:
containers:
- name: web
image: self-healing-web:v1
imagePullPolicy: IfNotPresent
ports:
- containerPort: 80
env:
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
What does this Deployment define?
- The replica count declares that two Pods should run.
- The selector matches Pods carrying the app: self-healing-web label.
- The container uses the local self-healing-web:v1 image.
- The IfNotPresent policy keeps Kubernetes on the local image when it is already available.
- The field path metadata.name places each generated Pod name into POD_NAME.
- Save kubernetes.yaml.
Before you create the Deployment, do you expect Kubernetes to start one Pod or two?
- Create the initial Deployment by running:
kubectl apply -f kubernetes.yaml
What does this command do?
The apply command sends the manifest to the active cluster. Kubernetes creates the Deployment because it does not exist yet.
Your terminal confirms that the manifest was accepted.
- Wait for both replicas to become available by running:
kubectl rollout status deployment/self-healing-web
What does rollout status check?
This command watches the Deployment until its requested replicas are available. A successful result confirms that the cluster started the local image.
You should see the rollout complete successfully. Your cluster now maintains two running replicas.
Deployment rollout stuck?
Confirm that Docker Desktop still uses the containerd image store. The kind provisioner depends on that image store.
Check that the local image tag is exactly self-healing-web:v1. A different tag leaves the Deployment without the image it requests.
Help me troubleshoot the stalled Deployment.
The replicas are running. Now you can make their replacement behavior explicit with a controlled rolling strategy.
- In kubernetes.yaml, locate the selector followed by the Pod template.
- Find these lines:
selector:
matchLabels:
app: self-healing-web
template:
metadata:
What are these lines?
The selector identifies the Pods owned by the Deployment. The template begins the definition used to create each Pod.
- Insert the rolling-update strategy between the selector and template so the section looks like this:
selector:
matchLabels:
app: self-healing-web
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 0
maxSurge: 1
template:
metadata:
How does the rolling strategy work?
The maxUnavailable: 0 setting keeps every requested replica available during an update. The maxSurge: 1 setting allows one extra Pod while a replacement starts.
- Save kubernetes.yaml.
- Apply the explicit rolling strategy by running:
kubectl apply -f kubernetes.yaml
What changed in the cluster?
Kubernetes updates the Deployment configuration with the rolling limits. The container template stays unchanged during this apply.
Your terminal confirms that the Deployment configuration was accepted.
The Deployment manages the replicas. The Service now gives those replaceable Pods one stable target.
- Append the ClusterIP Service to the end of kubernetes.yaml using this code:
---
apiVersion: v1
kind: Service
metadata:
name: self-healing-web
spec:
type: ClusterIP
selector:
app: self-healing-web
ports:
- name: http
protocol: TCP
port: 80
targetPort: 80
What does this Service define?
The Service selects every Pod carrying the matching application label. Its stable name remains available even when individual Pods change.
Port 80 on the Service sends traffic to port 80 on a selected Pod.
- Save kubernetes.yaml.
✔️ Awesome, I've got everything!
Great. Double check that kubernetes.yaml is saved in the self-healing-web folder.
ⓧ I'd like to double check the full code
apiVersion: apps/v1
kind: Deployment
metadata:
name: self-healing-web
labels:
app: self-healing-web
spec:
replicas: 2
selector:
matchLabels:
app: self-healing-web
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 0
maxSurge: 1
template:
metadata:
labels:
app: self-healing-web
spec:
containers:
- name: web
image: self-healing-web:v1
imagePullPolicy: IfNotPresent
ports:
- containerPort: 80
env:
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
---
apiVersion: v1
kind: Service
metadata:
name: self-healing-web
spec:
type: ClusterIP
selector:
app: self-healing-web
ports:
- name: http
protocol: TCP
port: 80
targetPort: 80
Deploy and open the app
The completed manifest now contains both resources. Applying it creates the Service while preserving the healthy Deployment.
- Apply the completed manifest by running:
kubectl apply -f kubernetes.yaml
What happens during this apply?
Kubernetes reconciles the Deployment with the saved manifest. It also creates the ClusterIP Service that targets the matching Pods.
Your terminal confirms that both resource definitions were accepted.
- Confirm the Deployment remains healthy by running:
kubectl rollout status deployment/self-healing-web
What proves the rollout worked?
A successful rollout means the Deployment reached its desired state. Both replicas are available to receive Service traffic.
Your terminal reports that the rollout completed successfully.
Before you start the tunnel, which runtime identity do you expect the page to show?
- Create a second integrated terminal in VS Code using the terminal panel's new-terminal control.
- Forward local port 8081 to the Service by running:
kubectl port-forward service/self-healing-web 8081:80
How does the port-forward work?
The command opens a local tunnel to one Pod selected by the Service. It keeps running in this terminal until you stop it.
If the selected Pod terminates later, the forwarding session ends. Restarting the command connects through the Service again.
- Open http://localhost:8081 in your browser.
You should see the Version 1 page with a generated Pod name instead of docker-container. This proves the request reached a Kubernetes-managed replica.
- Record the generated Pod identity here: <pod-name-shown-in-browser>.
Page not loading on port 8081?
Keep the port-forward command running in its terminal. Closing that terminal removes the local tunnel.
Check the rollout terminal for a successful result. An incomplete Deployment leaves the Service without an available Pod.
Help me troubleshoot the Kubernetes Service page.
Your app has returned through two managed replicas and one stable Service endpoint. Next, you will scale the Deployment and delete the exact Pod you just recorded to prove Kubernetes can replace it.
Scale It and Prove Self-Healing
Your two-replica Kubernetes Deployment now serves Version 1 through one stable Service. That setup brought the page back after the standalone container outage.
Self-healing becomes visible when desired state survives disruption. You will increase the desired replica count to three.
Then you will delete the exact Pod serving your browser. The Deployment should create a replacement without your help.
In this step, get ready to:
- Scale the Deployment from two replicas to three replicas.
- Delete the exact Pod serving your browser.
- Prove the Deployment returns to three healthy replicas.
Increase the workload capacity
Scaling changes the Deployment's desired replica count. The controller starts another Pod to close the gap between the current count and the new target.
- Use the plus button in the integrated terminal from earlier to create a second terminal in Visual Studio Code.
- Scale the Deployment to three replicas by running this command:
kubectl scale deployment/self-healing-web --replicas=3
What does this command do?
- The scale request updates the Deployment's desired replica count to three.
- The Deployment controller creates one more Pod to reach that target.
Before you check, how many Pods do you expect to show Running? Hold that prediction for the next command.
- Check the matching Pods by running this command:
kubectl get pods -l app=self-healing-web
What should I see?
- The label filter limits the output to Pods belonging to the app.
- You should see three Pod rows with Running in the status column.
- Repeat the same check until all three rows show Running.
That capacity change is now real: the Deployment keeps three copies of your app running.
Still seeing fewer than three Pods?
A new Pod can take a few seconds to start while Kubernetes schedules it on the local node.
- Confirm Docker Desktop is still running.
- Rerun the scale command exactly as shown above.
- Repeat the Pod check until three matching rows appear.
Help me understand why my Deployment is not reaching three running Pods.
Test the controller's recovery
A watch keeps the terminal updated as Pod state changes. This gives you live evidence of the controller replacing the instance you remove.
- Switch back to the browser tab at http://localhost:8081.
- Record the Pod identity shown on the page here: <pod-name-shown-in-browser>.
- Return to the second integrated terminal from the previous substep.
- Start streaming changes to the matching Pods by running this command:
kubectl get pods -l app=self-healing-web --watch
What does this command do?
- The label filter keeps the stream focused on this app's Pods.
- The --watch flag keeps the command active as Pod states change.
- Use the plus button in the integrated terminal to create a third terminal.
Before you delete this Pod, do you expect the Deployment to stay below three replicas? Hold that prediction for the watch output.
- Delete the Pod recorded from your browser by running this command:
kubectl delete pod [[POD_NAME_SHOWN_IN_BROWSER="<pod-name-shown-in-browser>"]]
What does this command prove?
The delete request removes one managed Pod. The Deployment controller notices that only two replicas remain and creates another Pod to restore its target of three.
The watch shows the recorded Pod begin to terminate. A different Pod name appears as Kubernetes creates its replacement.
The original port-forward terminal also stops. That interruption is expected because the forwarding session was attached to the Pod you deleted.
- Keep watching until the new Pod shows Running.
There is your recovery proof: Kubernetes restored the replica count after you removed a live instance.
No replacement Pod appearing?
A replacement can take a few seconds to start on the local cluster.
- Keep the watch terminal active for a few more seconds.
- Rerun the scale command from the previous substep if the desired replica count changed.
- Confirm the watched Pods use the app=self-healing-web label.
Help me find out why Kubernetes did not replace my deleted Pod.
Reconnect to the recovered app
The Service survived the Pod deletion because it selects whichever matching Pods are healthy. Only the local forwarding session ended with the deleted Pod.
- Press Control+C in the watch terminal to stop the stream.
- Return to the port-forward terminal from earlier.
- Restart access through the Service by running this command:
kubectl port-forward service/self-healing-web 8081:80
What does this command do?
This creates a fresh forwarding session from local port 8081 to port 80 on the Service. The Service routes the connection to a live matching Pod.
- Switch back to the browser tab at http://localhost:8081.
- Refresh the page.
You should see Version 1 with a live Pod identity that differs from the Pod you deleted.
Before the final check, how many Pods should the Deployment have after replacing the deleted one? Hold that prediction for the command output.
- Verify the final replica count by running this command in the third terminal:
kubectl get pods -l app=self-healing-web
What does this check prove?
- You should see three matching Pods with Running in the status column.
- The replacement has a different generated name from the Pod you deleted.
- The browser confirms the Service still reaches the application.
That is self-healing proven: your app survived a live Pod deletion and returned to its desired state of three replicas.
Browser still unavailable?
- Keep the restarted port-forward command running in its terminal.
- Refresh http://localhost:8081 after all three Pods show Running.
- Stop any leftover forwarding process if the new command reports that the local port cannot be used.
Help me reconnect to the recovered app through the Kubernetes Service.
Secret mission
Roll Out Version 2
Give the status page a new look and package it as a second immutable image. Then update the live Deployment while Kubernetes replaces the three replicas at a controlled rate. The browser finishes on Version 2 with a live Pod identity.
Clean Up Your Resources
Clean Up Your Resources
Your local Docker and Kubernetes resources use CPU, memory, and disk without creating direct cloud charges. Decide whether to keep them running, pause the workload, or remove them.
Resources you used:
- A Deployment with its rollout ReplicaSets, three running Pods, and a ClusterIP Service.
- An active kubectl port-forward that exposes the Service on local port 8081.
- A one-node kind cluster running in Docker Desktop.
- Two local Docker images tagged self-healing-web:v1 and self-healing-web:v2.
- The self-healing-web folder containing index.html.template, Dockerfile, and kubernetes.yaml.
Keep everything running
No action is needed. Choose this if you want to keep testing the Version 2 page or experimenting with self-healing.
- Leave the active Service port-forward running.
- Keep Docker Desktop running with all three Pods active.
- Keep both image tags available for future rollouts.
- Keep the self-healing-web folder open in VS Code.
- Expect the cluster to continue using local CPU and memory.
Pause - I'll come back to this later
Stop the running workload to free local CPU and memory. Your Kubernetes configuration, images, and project files remain available for later.
- Switch back to the terminal running the Service port-forward.
- Press Ctrl+C to stop the port-forward.
- Pause the workload by running these commands:
kubectl scale deployment/self-healing-web --replicas=0
kubectl get pods -l app=self-healing-web
What do these commands do?
- The first command changes the Deployment's desired replica count to 0.
- The second command lists the Pods that match the app label.
- Repeat the Pod check until no application Pods are listed.
Your workload is now quiet while its configuration stays ready for your return.
- When you return, restore three replicas by running this command:
kubectl scale deployment/self-healing-web --replicas=3
How does this resume the workload?
The Deployment restores its desired state of three replicas. Kubernetes creates three Pods from the Version 2 image.
- Restart browser access in another terminal by running this command:
kubectl port-forward service/self-healing-web 8081:80
How does browser access return?
The command forwards local port 8081 to the Service. The terminal remains attached while the forwarding session is active.
Delete - I don't want to use this again
This path permanently removes the project workload, both image tags, and the self-healing-web folder. You have already proved the rolling update, so the evidence of your learning stays with you.
- Switch back to the terminal running the Service port-forward.
- Press Ctrl+C to stop the port-forward.
- Delete the resources declared in kubernetes.yaml by running this command:
kubectl delete -f kubernetes.yaml
What does this command remove?
The manifest identifies the self-healing web Deployment and Service for deletion. The managed ReplicaSets and Pods are removed with the workload.
The kind cluster stays available until you stop it separately.
- Remove both local image tags by running these commands:
docker image rm self-healing-web:v1
docker image rm self-healing-web:v2
What do these commands remove?
Each command removes one versioned image tag from Docker's local image store. The Version 1 and Version 2 application images are no longer available for new containers.
- Return to Docker Desktop's Kubernetes view.
- Select Stop to stop the local cluster.
The cluster no longer consumes runtime resources after it stops.
- Switch back to the VS Code integrated terminal in the self-healing-web folder.
Folder deletion is permanent
The next command removes the project files from your Mac. Keep a copy elsewhere before continuing if you want to reuse the manifest or status page.
- Remove the self-healing-web folder by running these commands:
cd ..
rm -rf self-healing-web
ls
What do these commands do?
- The first command moves the terminal to the folder above self-healing-web.
- The second command permanently deletes the project folder.
- The final command lists the remaining folders so you can confirm that self-healing-web is gone.
That completes the local cleanup. Your project workload, image tags, cluster process, and files are no longer consuming resources.
Nice Work!
Nice Work!
You did it! Your versioned Docker image now serves a live Pod identity across three self-healing Kubernetes replicas.
You've learned how to:
- Built a custom versioned NGINX image from Dockerfile. Ran it as one named container that displayed its runtime identity in the browser.
- Declared the app's desired state with a Deployment and a ClusterIP Service. Exposed each Pod's generated name through the Downward API.
- Scaled the workload to three replicas. Proved self-healing when the Deployment replaced the exact Pod you deleted.
- Secret Mission: Built self-healing-web:v2. Rolled it across three replicas with maxUnavailable: 0 and maxSurge: 1. Verified the Version 2 page with a live Pod identity.
Ready to quiz yourself?