Kubernetes Self-Healing Delivery
Build a local Kubernetes pipeline that deploys and heals an NGINX service.
Introduction
30 Second Summary
A service can keep running while quietly losing the ability to handle requests. Without an automatic recovery loop, someone has to notice the failure before users feel it.
In this project, you will build a self-healing local Kubernetes delivery loop for a service-status page packaged as a Docker container. Each GitHub push triggers GitHub Actions to deploy the page through a self-hosted runner.
What You'll Build
Your finished demo turns one Git push into a tested local rollout that restores the status page after you deliberately break its health endpoint.
By the end of this project, you'll have:
- A browser-visible status page at http://localhost:8080 with a /health endpoint that returns ok.
- A self-healing recovery demo where removing /health makes Kubernetes restart the unhealthy container.
- A push-triggered delivery loop that carries each change from GitHub to a verified Minikube rollout.
- Secret Mission: Add a readiness probe that removes an unhealthy Pod from Service traffic before its liveness restart.
Are there any prerequisites?
This project requires a Windows x64 computer with virtualization enabled. Your computer also needs at least 2 CPUs, 2 GB of free memory, 20 GB of free disk space, and internet access.
The project assumes Docker Desktop, Git, and Visual Studio Code are installed. You also need access to a GitHub account.
Before We Start
Before the hands-on work begins, this checkpoint connects your push-triggered local Kubernetes deployment to the recovery behavior you will demonstrate.
Start a Local Kubernetes Control Plane
Docker can run a single container. Automatic recovery needs a Kubernetes control plane that compares your desired state with what is actually running.
Minikube gives you that control plane as a local cluster on your computer. This step makes sure the foundation is healthy before you build the service.
In this step, get ready to:
- Confirm Docker Desktop is running with Linux containers.
- Install Minikube v1.39.0 in Administrator PowerShell.
- Bring up a healthy Minikube cluster with the Docker driver.
Confirm Docker and install Minikube
Minikube's Docker driver depends on Docker Desktop's Linux engine. Administrator PowerShell gives the installer the permissions it needs.
- Press the Windows key to open Windows search.
- Type Docker Desktop and press Enter.
- Wait for Docker Desktop to finish starting.
- Press the Windows key to return to Windows search.
- Type PowerShell into the search field.
- Start PowerShell with administrator privileges from the search results.
- Approve the Windows permission request if one appears.
- Check Docker Desktop's container type by running this command:
docker info --format '{{.OSType}}'
The command should print linux. This confirms Minikube can use Docker Desktop's Linux engine.
Docker reports the wrong container type?
- Switch Docker Desktop to Linux containers with its container-mode control.
- Wait for Docker Desktop to restart.
- Repeat the container type check in Administrator PowerShell.
Help me configure Docker Desktop to use Linux containers.
Docker is ready for Minikube. You have confirmed the exact engine the local cluster needs.
- Check whether Minikube is already installed by running this command:
minikube version
✔️ I see the required version
Your output includes v1.39.0. Minikube is ready to create the local cluster.
- Keep Administrator PowerShell open for the cluster startup.
ⓧ I see an older version
The cluster in this project uses features verified against Minikube v1.39.0. Update Minikube before continuing.
- Open the official Minikube release page for v1.39.0.
- Download minikube-installer.exe from the release assets.
- Run minikube-installer.exe from your Downloads folder.
- Complete the installer using its default options.
- Close the current Administrator PowerShell window.
- Reopen Administrator PowerShell from Windows search.
- Confirm the updated version by running this command:
minikube version
Your output should now include v1.39.0.
Still seeing the older version?
Close every PowerShell window before opening a fresh Administrator PowerShell session. This refreshes the command path after installation.
Help me find why PowerShell still detects my older Minikube version.
ⓧ Command not found
Minikube is absent from your command path. Windows Package Manager can install the required release.
- Install Minikube by running this command:
winget install Kubernetes.minikube
The command completes after Windows installs Minikube. A Windows permission request may appear during installation.
Windows Package Manager unavailable?
- Open the official Minikube release page for v1.39.0.
- Download minikube-installer.exe from the release assets.
- Run the downloaded installer from your Downloads folder.
Help me install Minikube without Windows Package Manager.
- Close the current Administrator PowerShell window after installation finishes.
- Reopen Administrator PowerShell from Windows search.
- Confirm the installed version by running this command:
minikube version
Your output should include v1.39.0.
Start the local cluster
The Docker driver places the local control plane inside Docker Desktop. The first startup downloads the required cluster components, so expect it to take a few minutes.
- Start the local cluster with the Docker driver by running this command:
minikube start --driver=docker
When the command returns to the prompt, Minikube has created the local cluster. That cluster uses Kubernetes v1.37.0.
Its container runtime is containerd. That's the local control plane running on your computer.
Cluster startup failed?
- Confirm Docker Desktop is still running.
- Repeat the Linux container type check from the previous substep.
- Confirm virtualization remains enabled on your computer.
Help me diagnose my Minikube startup failure.
Verify the control plane
Cluster status reports whether the host, kubelet, and API server are healthy. Minikube's bundled kubectl client lets you inspect the Kubernetes system Pods without installing another command-line tool.
Before you run these checks, picture the states you expect for the control-plane components and system Pods.
- Check the control-plane components by running this command:
minikube status
The host, kubelet, and API server should each report Running.
Control plane not running?
- Confirm Docker Desktop is still running.
- Repeat the cluster startup command from the previous substep.
- Run the status check again after startup completes.
Help me restore my Minikube control plane to a running state.
- Inspect every Kubernetes system Pod through the bundled client by running this command:
minikube kubectl -- get pods -A
The system Pods should settle into Running or Completed states. This proves the bundled client can reach the healthy control plane.
System Pods still settling?
- Wait a moment for the system Pods to finish starting.
- Repeat the Pod check above.
Help me understand why my Minikube system Pods are not ready.
✔️ My cluster checks match
You have a healthy local Kubernetes control plane. Minikube can now run the service you build in the next step.
ⓧ I'd like to double check the cluster state
Your completed environment should match each checkpoint below.
- Docker Desktop is running with Linux containers.
- Minikube v1.39.0 is available in Administrator PowerShell.
- The local Minikube cluster uses the Docker driver.
- The control plane runs Kubernetes v1.37.0.
- The cluster uses containerd as its container runtime.
- The bundled minikube kubectl -- client can reach the cluster.
- The host, kubelet, and API server report Running.
- The Kubernetes system Pods report Running or Completed.
Your local Kubernetes control plane is healthy and accessible. Next, you will build the first browser-visible service that this cluster eventually keeps alive.
Build the Visible Service
Your local Kubernetes control plane is running. It now needs a real container image that you can see working before orchestration enters the picture.
This service gives you a status page plus a separate health endpoint. That endpoint becomes the signal Kubernetes uses to judge the service later.
In this step, get ready to:
- Create the status page and health endpoint files.
- Build a pinned NGINX image named cloudops-autopilot:local.
- Run the service locally and verify both browser URLs.
Create the service files
The cloudops-autopilot folder keeps the page and image recipe together. You will create it as a workspace in Visual Studio Code.
- Press the Windows key to open Windows search.
- Type Visual Studio Code and press Enter.
- Select File from the top menu.
- Select Open Folder....
- Select your Desktop.
- Select New Folder.
- Name the folder cloudops-autopilot.
- Select Select Folder.
Seeing a workspace trust prompt?
Select Yes, I trust the authors because you created this folder. This enables the editor features you use in the workspace.
The empty cloudops-autopilot folder now appears in the Explorer view. Each file you add becomes part of the image build context.
- Select New File... in the Explorer view.
- Enter Dockerfile and press Enter.
- Select New File... again.
- Enter index.html and press Enter.
- Select New File... again.
- Enter health and press Enter.
You should see Dockerfile, index.html, and health under cloudops-autopilot in the Explorer view.
- Select index.html in the Explorer view.
- Add the page structure using this code:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>CloudOps Autopilot</title>
</head>
<body>
<main class="panel">
<p class="eyebrow">LOCAL KUBERNETES DELIVERY</p>
<h1>CloudOps Autopilot</h1>
<p class="status">Built and verified locally.</p>
<ul>
<li>Container image: ready</li>
<li>Kubernetes deployment: running</li>
<li>Health endpoint: ready</li>
</ul>
</main>
</body>
</html>
What does this page structure do?
- The metadata makes the page render consistently across desktop and mobile browsers.
- The panel groups the service name with its current delivery status.
- The status list makes the image and endpoint state visible during a demonstration.
- Save index.html by pressing Ctrl+S.
- Right-click index.html in the Explorer view.
- Select Reveal in File Explorer.
- Double-click index.html in File Explorer.
Your browser should show the CloudOps Autopilot heading and the three status items. The page is unstyled at this point, which confirms that the HTML structure works.
Seeing a blank page?
Confirm that index.html is saved inside cloudops-autopilot. Check that the closing </html> line is present.
Ask for help if the file still opens without content: help me diagnose a blank local HTML page
The page now needs a dark canvas that centers the status card. These base styles establish that layout before the detailed card styling is added.
- Return to index.html in Visual Studio Code.
- Find the </head> line.
- Insert this style block directly above that line:
<style>
:root {
color-scheme: dark;
font-family: Inter, system-ui, sans-serif;
background: #07111f;
color: #e8f2ff;
}
body {
min-height: 100vh;
margin: 0;
display: grid;
place-items: center;
background: radial-gradient(circle at top, #12345a, #07111f 60%);
}
</style>
What do the base styles control?
- The root styles set the dark colour scheme and the page typography.
- The body fills the browser height and centers the service panel.
- The radial background creates a visible focal point behind the status card.
- Save index.html.
- Refresh the page in your browser.
You should see the content centered on a dark blue background. This confirms that the browser is loading the new style block.
Still seeing a white background?
Check that the <style> block sits inside <head>. Confirm that you saved the file before refreshing.
Ask for help if the browser keeps showing the old version: help me find why my CSS changes are not loading
The centered content needs a visual boundary. The next styles turn it into a card with a small delivery label.
- Find the </style> line in index.html.
- Insert these selectors directly above that line:
.panel {
width: min(620px, calc(100% - 48px));
padding: 40px;
border: 1px solid #2c5f8a;
border-radius: 18px;
background: rgba(8, 25, 43, 0.92);
box-shadow: 0 24px 80px rgba(0, 0, 0, 0.35);
}
.eyebrow {
color: #63d3ff;
font-size: 0.78rem;
font-weight: 700;
letter-spacing: 0.16em;
}
What do these selectors style?
- The .panel selector gives the service information a responsive width and card shape.
- The .eyebrow selector turns the delivery label into a compact visual marker.
- Save index.html.
- Refresh the page in your browser.
You should see a rounded panel with a blue border and a bright delivery label. The status page now has a clear visual frame.
Missing the panel styling?
Check that .panel matches class="panel". Confirm that both selectors remain above </style>.
Ask for help if the selectors still have no effect: help me debug the panel CSS
The final styles separate the current status from the supporting checklist. This makes the local verification sentence the strongest signal on the page.
- Find the </style> line again.
- Insert these final selectors directly above that line:
h1 {
margin: 8px 0 12px;
font-size: clamp(2rem, 6vw, 3.6rem);
}
.status {
padding: 14px 16px;
border-left: 4px solid #43e6a8;
background: #0c2d2a;
color: #baffdf;
}
li {
margin: 10px 0;
}
What do the final styles change?
- The heading scales with the browser width while keeping its spacing predictable.
- The status sentence gains a green accent and its own background.
- The list spacing keeps the three service checks easy to scan.
- Save index.html.
- Refresh the page in your browser.
You should see a large CloudOps Autopilot heading above a green-accented status box. The box should read Built and verified locally..
Status box still looks plain?
Confirm that .status matches class="status". Check that every CSS selector has a closing brace.
Ask for help if only part of the page is styled: help me locate a CSS syntax problem
The page is ready to package. Docker uses the Dockerfile to start from a pinned NGINX image and copy both service files into its static content directory.
- Select Dockerfile in the Explorer view.
- Add the image recipe using this code:
FROM nginx:1.31.5-alpine3.24@sha256:72ba65eb42c10344912a84ff42408db7d34f2feb642204570ab8fc5ffd29f1d3
COPY index.html /usr/share/nginx/html/index.html
COPY health /usr/share/nginx/html/health
What does the Dockerfile do?
- The FROM instruction fixes the NGINX tag and image digest used by every build.
- The first COPY instruction replaces the default page with index.html.
- The second COPY instruction exposes the health response as a static file.
- Save Dockerfile.
Dockerfile showing as plain text?
A Dockerfile has no file extension. Confirm that its name is exactly Dockerfile and does not end in .txt.
Ask for help if the filename looks different in the Explorer view: help me correct my Dockerfile name
The health endpoint needs a tiny response that is cheap to request repeatedly. Its entire body is one word.
- Select health in the Explorer view.
- Add the health response using this code:
ok
Why is the health response so small?
A health check only needs an inexpensive success response. The ok body makes the result obvious in a browser without adding application logic.
- Save health.
Health file has an extension?
Confirm that the filename is exactly health with no extension. Rename it in the Explorer view if Visual Studio Code shows another suffix.
Ask for help if you cannot remove the suffix: help me create an extensionless health file
✔️ Awesome, I've got everything!
Your three service files are ready. Make sure Dockerfile, index.html, and health are saved.
ⓧ I'd like to double check the full code
Compare each file with these complete versions before building the image.
FROM nginx:1.31.5-alpine3.24@sha256:72ba65eb42c10344912a84ff42408db7d34f2feb642204570ab8fc5ffd29f1d3
COPY index.html /usr/share/nginx/html/index.html
COPY health /usr/share/nginx/html/health
What should match in Dockerfile?
The base image must include the complete tag and digest. Both copy destinations must point to the NGINX static content directory.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>CloudOps Autopilot</title>
<style>
:root {
color-scheme: dark;
font-family: Inter, system-ui, sans-serif;
background: #07111f;
color: #e8f2ff;
}
body {
min-height: 100vh;
margin: 0;
display: grid;
place-items: center;
background: radial-gradient(circle at top, #12345a, #07111f 60%);
}
.panel {
width: min(620px, calc(100% - 48px));
padding: 40px;
border: 1px solid #2c5f8a;
border-radius: 18px;
background: rgba(8, 25, 43, 0.92);
box-shadow: 0 24px 80px rgba(0, 0, 0, 0.35);
}
.eyebrow {
color: #63d3ff;
font-size: 0.78rem;
font-weight: 700;
letter-spacing: 0.16em;
}
h1 {
margin: 8px 0 12px;
font-size: clamp(2rem, 6vw, 3.6rem);
}
.status {
padding: 14px 16px;
border-left: 4px solid #43e6a8;
background: #0c2d2a;
color: #baffdf;
}
li {
margin: 10px 0;
}
</style>
</head>
<body>
<main class="panel">
<p class="eyebrow">LOCAL KUBERNETES DELIVERY</p>
<h1>CloudOps Autopilot</h1>
<p class="status">Built and verified locally.</p>
<ul>
<li>Container image: ready</li>
<li>Kubernetes deployment: running</li>
<li>Health endpoint: ready</li>
</ul>
</main>
</body>
</html>
What should match in index.html?
The page should include every selector and the local status sentence. The content should remain inside the panel element.
ok
What should match in health?
The file should contain only ok.
Build and test the image
The browser preview proves the page itself works. Building the image now proves Docker can package that page with the pinned NGINX server.
- Select View from the Visual Studio Code menu.
- Select Terminal.
- Build and tag the local image by running this command:
docker build --tag cloudops-autopilot:local .
What does this build command do?
- The build reads Dockerfile from the current cloudops-autopilot folder.
- The tag gives the resulting image the reusable name cloudops-autopilot:local.
- The final dot supplies the current folder as the build context.
Docker should process the pinned base image and both copy instructions. A successful completion means the local image now contains your page and health file.
Image build failed?
Confirm that the terminal is inside the cloudops-autopilot folder. Check that all three filenames match the copy instructions exactly.
Confirm that Docker Desktop is still running with Linux containers. The build needs its local Docker engine.
Ask for help with the reported build output: help me diagnose this Docker build failure
Before you test the image, what result would convince you that NGINX can read its packaged configuration?
- Test the packaged NGINX configuration by running this command:
docker run --rm cloudops-autopilot:local nginx -t
What does this test prove?
Docker starts a temporary container from your new image and asks NGINX to test its configuration. The temporary container is removed after the test exits.
You should see NGINX report that its configuration syntax is valid. This confirms that the image can start its web server.
Configuration test did not pass?
Confirm that the build completed before this test. A failed or interrupted build can leave the expected local image unavailable.
Check the image name in the command for the exact cloudops-autopilot:local tag.
Ask for help with the test output: help me diagnose the NGINX configuration test
Run the service locally
The configuration test proves the server can start. Running the image with a published port lets your browser verify the exact page and endpoint that Kubernetes uses later.
Before you start the container, what should appear at the main URL compared with the health URL?
- Start the temporary service container by running this command:
docker run --name cloudops-local --rm --detach --publish 127.0.0.1:8080:80 cloudops-autopilot:local
What does this run command do?
- The container receives the name cloudops-local so you can stop it cleanly.
- Detached mode keeps it running while PowerShell returns control to you.
- The published port connects local port 8080 to NGINX port 80 inside the container.
PowerShell should return a container identifier. That confirms Docker started the service in the background.
Container did not start?
Confirm that the image build and configuration test succeeded. Check that another local process is not already using port 8080.
Ask for help with the command output: help me start the local service container
- Enter http://localhost:8080 in your browser address bar.
You should see the dark CloudOps Autopilot page with the status Built and verified locally..
- Enter http://localhost:8080/health in your browser address bar.
You should see ok by itself. That response proves the image serves a separate health signal alongside the visible page.
Page or health response missing?
Confirm that the address starts with http:// and uses port 8080. Check that PowerShell still shows the container start command as successful.
If the page loads but the health URL does not, confirm that the health file has no extension and contains only ok.
Ask for help with the result you see: help me debug the local page or health endpoint
That is your first complete service running from a custom image. The page is visible and its health signal answers independently.
- Stop the temporary service container by running this command:
docker stop cloudops-local
What does this stop command do?
Docker stops the container named cloudops-local. Its removal setting then clears the temporary container while keeping cloudops-autopilot:local available for reuse.
PowerShell should confirm the stopped container name. Your image remains ready for the running Minikube cluster.
Container name not found?
The container may already have stopped. Confirm that the start command used the exact cloudops-local name.
Ask for help if you are unsure whether it is still running: help me check my temporary Docker container
Your visible service is built and tested. Next, you will load the image into Minikube and discover what a Kubernetes Deployment cannot heal without a liveness signal.
Deploy Without Health Probes
Your local container image already serves the CloudOps Autopilot page. Now your Kubernetes cluster needs instructions for running that image.
A Deployment can keep one Pod running. A Service can route traffic to that Pod.
However, a running Pod can still host an unhealthy application. This first deployment deliberately omits health probes, which leaves Kubernetes without an application health signal.
In this step, get ready to:
- Define a one-replica Deployment in k8s/deployment.yaml.
- Expose the Pod through a Service.
- Deploy the preloaded local image to Minikube.
Create the probe-free manifest
A Kubernetes manifest records the desired state of your application. This file defines the workload first, followed by the network route that exposes it.
- In the Explorer sidebar, use the folder control beside cloudops-autopilot to create a folder named k8s.
- Inside k8s, use the file control to create deployment.yaml.
You should see k8s/deployment.yaml in the Explorer sidebar.
- Select k8s/deployment.yaml in the Explorer sidebar.
- Add the Deployment definition by pasting this code:
apiVersion: apps/v1
kind: Deployment
metadata:
name: cloudops-autopilot
spec:
replicas: 1
selector:
matchLabels:
app: cloudops-autopilot
template:
metadata:
labels:
app: cloudops-autopilot
spec:
containers:
- name: web
image: cloudops-autopilot:local
imagePullPolicy: Never
ports:
- name: http
containerPort: 80
What Does This Deployment Define?
- The replicas value asks Kubernetes to keep one Pod running.
- The app: cloudops-autopilot label connects the Deployment selector to its Pod template.
- The web container runs the image you built earlier.
- The imagePullPolicy: Never setting makes the node use an image from its local runtime.
- The named http port points to container port 80.
- Save k8s/deployment.yaml.
- Confirm the final line contains containerPort: 80.
Does the YAML Look Misaligned?
YAML uses spaces to represent nesting. Make sure each container field lines up beneath - name: web.
Keep ports beneath the container fields. Keep the named port beneath ports.
help me check the indentation in my Kubernetes Deployment
The Service gives the Deployment a stable network target. Its selector uses the same application label as the Pod template.
- Place your cursor after containerPort: 80.
- Append the Service definition by pasting this code:
---
apiVersion: v1
kind: Service
metadata:
name: cloudops-autopilot
spec:
selector:
app: cloudops-autopilot
ports:
- name: http
protocol: TCP
port: 80
targetPort: http
What Does This Service Define?
- The --- separator begins a second YAML document in the same file.
- The selector sends traffic to Pods carrying the app: cloudops-autopilot label.
- The Service receives TCP traffic on port 80.
- The targetPort: http value points to the named container port.
- Save k8s/deployment.yaml.
- Confirm the Explorer sidebar no longer shows an unsaved-change marker beside deployment.yaml.
Is the Service Missing From the File?
Check that the --- separator starts on a new line after the Deployment. The Service definition must remain in the same deployment.yaml file.
help me check both YAML documents
✔️ Awesome, I've got everything!
Great. Your saved manifest now contains the probe-free Deployment plus its Service.
ⓧ I'd like to double check the full code
Compare your complete k8s/deployment.yaml file with this version:
apiVersion: apps/v1
kind: Deployment
metadata:
name: cloudops-autopilot
spec:
replicas: 1
selector:
matchLabels:
app: cloudops-autopilot
template:
metadata:
labels:
app: cloudops-autopilot
spec:
containers:
- name: web
image: cloudops-autopilot:local
imagePullPolicy: Never
ports:
- name: http
containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: cloudops-autopilot
spec:
selector:
app: cloudops-autopilot
ports:
- name: http
protocol: TCP
port: 80
targetPort: http
How Is This File Structured?
This file contains two YAML documents. The separator keeps the Deployment definition separate from the Service definition.
Load and deploy the local image
The image currently lives in Docker Desktop. Minikube needs its own local copy before the Deployment can start the container.
- Load the local image into Minikube by running this command in the project terminal:
minikube image load cloudops-autopilot:local --daemon --overwrite
What Does This Command Do?
This command copies cloudops-autopilot:local from the Docker daemon into Minikube. The overwrite option replaces an older image with the same tag.
That local copy works with imagePullPolicy: Never, so the node does not contact an image registry.
- Wait for the PowerShell prompt to return without an error.
That handoff is complete. Minikube can now start the exact image you tested locally.
Did the Image Load Fail?
Confirm Docker Desktop is still running. Confirm the Minikube cluster from the previous step is still available.
help me diagnose a failed Minikube image load
Kubernetes can now read the saved manifest and create both resources. Applying the file moves the desired state into the cluster.
- Create the Deployment and Service by running this command:
minikube kubectl -- apply -f k8s/deployment.yaml
What Does This Command Do?
The bundled kubectl client reads k8s/deployment.yaml. Kubernetes creates or updates each resource described in the file.
You should see confirmation lines that name the cloudops-autopilot Deployment and Service.
Did Kubernetes Reject the Manifest?
Return to k8s/deployment.yaml and compare its indentation with the full-code tab. Check that both resource names use cloudops-autopilot.
help me diagnose my Kubernetes apply failure
Before you check the rollout, do you expect Kubernetes to find the locally loaded image and start one Pod?
- Wait for the Deployment rollout by running this command:
minikube kubectl -- rollout status deployment/cloudops-autopilot --timeout=120s
What Does This Command Check?
This command watches the latest Deployment rollout. It waits for up to 120s for the requested replica to become available.
You should return to the PowerShell prompt after the Deployment reports a successful rollout.
Is the Rollout Still Waiting?
Confirm the image-loading command completed successfully. Check that the manifest uses cloudops-autopilot:local with imagePullPolicy: Never.
help me diagnose a waiting Kubernetes rollout
Verify the service and restart count
The Service is available inside the cluster. Port forwarding maps its port to your computer so the browser can reach it at localhost:8080.
Before you connect, what do you expect the status page and health endpoint to show?
- Expose the Service on your computer by running this command:
minikube kubectl -- port-forward service/cloudops-autopilot 8080:80
What Does Port Forwarding Do?
This command forwards local port 8080 to Service port 80. Keep this terminal session running while you use the browser.
The forwarding session uses a selected Pod. A later Pod replacement can end the session.
- Leave the port-forward terminal running.
- Open http://localhost:8080 in your browser.
You should see the CloudOps Autopilot page with the status text Built and verified locally..
- Open http://localhost:8080/health in your browser.
You should see ok.
- Use the plus button in the terminal panel to create a second terminal.
Before you inspect the Pod, what restart count do you expect from this fresh deployment?
- Check the Pod state by running this command in the second terminal:
minikube kubectl -- get pods
What Does This Check Prove?
This command lists the Pods in the default namespace. The output shows whether the Deployment created a running Pod.
The restart count records how many times Kubernetes has restarted the container. This first deployment has not needed a restart.
You should see one Pod in the Running state with a restart count of zero. The working page plus the ok health response confirm that traffic reaches the probe-free deployment.
Can’t Reach the Service?
Confirm the first terminal still has the port-forward session running. Start the port-forward command again if that session ended.
If the Pod is not running, compare k8s/deployment.yaml with the full-code tab. Confirm the local image was loaded before the manifest was applied.
help me diagnose my probe-free Kubernetes deployment
Your service is live in Kubernetes with no health signal protecting it. Next, you’ll test how this deployment responds when the application becomes unhealthy.
Add Liveness Healing
Your Deployment now serves CloudOps Autopilot from a running Pod. The health endpoint proves the local Kubernetes cluster is reachable.
Deleting the health file leaves NGINX running. Kubernetes currently has no health signal that can trigger a restart.
In this step, get ready to:
- Reproduce a live-but-unhealthy container.
- Give Kubernetes an HTTP health signal.
- Watch the container restart after its health file disappears.
Prove the missing safety net
A web process can stay online after part of the application breaks. The missing health file creates that exact condition.
- Return to the http://localhost:8080/health browser tab from earlier.
You should see ok. This confirms the health file exists before the test.
Before you run the next command, consider whether Kubernetes can restart the Pod without a health signal.
- Trigger the live-but-unhealthy state by running this command:
minikube kubectl -- exec deploy/cloudops-autopilot -- rm -f /usr/share/nginx/html/health
How Does This Break Health?
The command removes the copy of health inside the running web container. The original file remains inside the local image.
NGINX continues serving the main page. Requests to /health can no longer return the expected response.
- Refresh the http://localhost:8080/health browser tab.
The page no longer shows ok. The container is alive while its health check is broken.
- Inspect the Pod state by running this command:
minikube kubectl -- get pods
What Does This Check Prove?
The Pod listing reports its current status. It also shows how many times the container has restarted.
You will still see the Pod in the Running state with zero restarts. This shortfall is intentional.
Kubernetes only sees a running process. It needs an application-level signal before it can detect this failure.
Health Check Still Looks Normal?
Confirm that the command targets the cloudops-autopilot Deployment. A different target leaves this container unchanged.
Check that your browser is showing /health. The main page continues working during this failure.
Help me reproduce the missing health file.
Teach Kubernetes what healthy means
A liveness probe asks whether a running container is still healthy. Repeated failures tell Kubernetes to restart that container.
- Switch back to k8s/deployment.yaml in Visual Studio Code.
- Find the containerPort: 80 line beneath the named http port.
- Place the liveness configuration directly below that port by adding this block:
livenessProbe:
httpGet:
path: /health
port: http
initialDelaySeconds: 5
periodSeconds: 5
timeoutSeconds: 2
failureThreshold: 2
How Does the Probe Work?
- The httpGet check requests /health through the named http port.
- The initialDelaySeconds value gives the container 5 seconds to start.
- The periodSeconds value repeats the check every 5 seconds.
- The timeoutSeconds value gives each request 2 seconds to respond.
- The failureThreshold value triggers a restart after 2 consecutive failures.
- Save k8s/deployment.yaml.
- Apply the liveness-enabled manifest by running this command:
minikube kubectl -- apply -f k8s/deployment.yaml
What Does Apply Change?
Kubernetes updates the existing Deployment from the saved manifest. The changed Pod template starts a new rollout with the liveness probe attached.
- Confirm the updated Deployment finishes rolling out by running this command:
minikube kubectl -- rollout status deployment/cloudops-autopilot --timeout=120s
What Does Rollout Status Prove?
The command waits for the replacement Pod to become available. A successful result confirms that Kubernetes accepted the updated Deployment.
Good progress. Your replacement Pod is running with the new health rule active.
Rollout Not Completing?
Check that livenessProbe lines up with ports beneath the web container. YAML indentation determines which object owns each setting.
Confirm that k8s/deployment.yaml was saved before you applied it.
Help me fix the liveness-enabled rollout.
✔️ Awesome, I've got everything!
Your saved k8s/deployment.yaml now contains the liveness probe. The Service configuration remains unchanged.
ⓧ I'd like to double check the full code
Compare your complete k8s/deployment.yaml with this final liveness-enabled version:
apiVersion: apps/v1
kind: Deployment
metadata:
name: cloudops-autopilot
spec:
replicas: 1
selector:
matchLabels:
app: cloudops-autopilot
template:
metadata:
labels:
app: cloudops-autopilot
spec:
containers:
- name: web
image: cloudops-autopilot:local
imagePullPolicy: Never
ports:
- name: http
containerPort: 80
livenessProbe:
httpGet:
path: /health
port: http
initialDelaySeconds: 5
periodSeconds: 5
timeoutSeconds: 2
failureThreshold: 2
---
apiVersion: v1
kind: Service
metadata:
name: cloudops-autopilot
spec:
selector:
app: cloudops-autopilot
ports:
- name: http
protocol: TCP
port: 80
targetPort: http
How to Compare the File
Use this complete file as a comparison reference. The only change from the earlier manifest is the livenessProbe block beneath the container port.
Watch Kubernetes repair the failure
The Deployment now has the missing health signal. Repeating the same failure tests whether the recovery loop acts on it.
- Return to the port-forward terminal from earlier.
- Restore browser access if the forwarding session ended by running this command:
minikube kubectl -- port-forward service/cloudops-autopilot 8080:80
Why Might Forwarding End?
The Minikube command forwards local port 8080 to the Kubernetes Service. A rollout can end a forwarding session when its selected Pod terminates.
The forwarding process stays attached to that terminal.
- Keep the forwarding terminal running.
- Switch back to the project PowerShell terminal from earlier.
Before you trigger the failure again, predict which value will change in the Pod watch.
- Remove the health file again by running this command:
minikube kubectl -- exec deploy/cloudops-autopilot -- rm -f /usr/share/nginx/html/health
What Happens Inside the Container?
The running container loses its health file again. The liveness probe now detects that missing response.
- Start a live Pod watch by running this command:
minikube kubectl -- get pods -w
What Does the Watch Show?
The watch streams Pod state changes into the terminal. The RESTARTS column records container restarts.
You will see the RESTARTS value increase. The Pod returns to the Running state after the container restarts.
- Stop the Pod watch by pressing Ctrl+C after the restart count increases.
That is the recovery loop working. Kubernetes detected the unhealthy container and restored it from the unchanged local image.
- Return to the port-forward terminal.
- Reopen forwarding if the session ended by running this command:
minikube kubectl -- port-forward service/cloudops-autopilot 8080:80
Why Reopen the Connection?
Port forwarding reconnects your browser to the recovered service. It provides the final visible check of the restored health file.
- Refresh http://localhost:8080/health in your browser.
You should see ok again. The restarted container restored the file from cloudops-autopilot:local.
Restart Count Not Increasing?
Confirm that the saved manifest contains livenessProbe beneath the web container.
Allow the probe enough time to record two failed checks. The configured threshold requires both failures before the restart.
Help me diagnose the missing liveness restart.
Your cluster now repairs a live-but-unhealthy container. Next up is push-triggered delivery.
Automate Delivery with GitHub Actions
Your Kubernetes liveness probe already recovers the service when its health file disappears. However, each deployment still depends on you rebuilding the image and running the rollout commands.
GitHub Actions turns every push into a repeatable job on your computer.
A Windows self-hosted runner rebuilds the container image. It then loads that image into Minikube for a verified rollout.
In this step, get ready to:
- Prepare a private GitHub repository with a Windows x64 self-hosted runner.
- Add the deployment workflow and update the page status.
- Push the project to main and verify the automated rollout.
Create the private repository and runner
A private GitHub repository stores the project and controls which workflow reaches your computer. The runner connects that repository to the Docker daemon and Minikube cluster already running on your machine.
- In GitHub, start a new repository.
- Enter cloudops-autopilot as the repository name.
- Set the repository visibility to private.
- Leave the repository initialization options empty.
- Create the repository.
Why use a self-hosted runner?
GitHub-hosted machines cannot reach the Minikube cluster on your computer.
A self-hosted runner executes the workflow locally. This gives the workflow access to your existing Docker Desktop engine and Minikube cluster.
- In the repository navigation, select Settings.
- In the sidebar, select Actions.
- Select Runners.
- Choose New self-hosted runner.
- Select Windows as the operating system.
- Select x64 as the architecture.
Protect your runner
Workflow code runs directly on your computer. Keep this repository private so untrusted contributions cannot execute jobs on the runner.
The setup page embeds a time-limited registration token in one generated command. Run that command directly without sharing or saving the token elsewhere.
- Return to the Administrator PowerShell terminal from earlier.
- Run GitHub's generated download commands in their displayed order.
- Run GitHub's generated configuration command.
- Answer no when the configuration asks whether to install the runner as a Windows service.
- Run GitHub's final generated command to start the runner interactively.
- Keep this runner terminal open for the rest of the project.
You will see Listening for Jobs in the terminal. That message confirms the repository can now send work to your computer.
Runner not connecting?
- Refresh the runner setup page if the generated registration token is more than one hour old.
- Confirm you ran the generated Windows commands in Administrator PowerShell.
- Check that the runner terminal remains open after configuration.
Ask for help with connecting the Windows runner.
Add the workflow and deployment message
The status sentence gives you visible proof that the automated workflow deployed a new image. You will change that sentence before defining the workflow that delivers it.
- In index.html, find this line:
<p class="status">Built and verified locally.</p>
What does this line show?
This status text identifies the version that you built manually. Changing it creates a visible marker for the automated deployment.
- Replace the existing status line with this updated line:
<p class="status">Deployed automatically by GitHub Actions.</p>
What changes here?
The page now identifies GitHub Actions as its delivery path. Seeing this exact sentence after the rollout proves the new image reached Kubernetes.
- Save index.html.
The saved file now contains the deployment message that the workflow will package into the image.
Can't find the status line?
Search index.html for Built and verified locally..
Ask for help with updating the status sentence.
✔️ Awesome, I've got everything!
Your saved index.html now identifies the automated deployment.
ⓧ I'd like to double check the full code
- Compare your complete index.html with this file:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>CloudOps Autopilot</title>
<style>
:root {
color-scheme: dark;
font-family: Inter, system-ui, sans-serif;
background: #07111f;
color: #e8f2ff;
}
body {
min-height: 100vh;
margin: 0;
display: grid;
place-items: center;
background: radial-gradient(circle at top, #12345a, #07111f 60%);
}
.panel {
width: min(620px, calc(100% - 48px));
padding: 40px;
border: 1px solid #2c5f8a;
border-radius: 18px;
background: rgba(8, 25, 43, 0.92);
box-shadow: 0 24px 80px rgba(0, 0, 0, 0.35);
}
.eyebrow {
color: #63d3ff;
font-size: 0.78rem;
font-weight: 700;
letter-spacing: 0.16em;
}
h1 {
margin: 8px 0 12px;
font-size: clamp(2rem, 6vw, 3.6rem);
}
.status {
padding: 14px 16px;
border-left: 4px solid #43e6a8;
background: #0c2d2a;
color: #baffdf;
}
li {
margin: 10px 0;
}
</style>
</head>
<body>
<main class="panel">
<p class="eyebrow">LOCAL KUBERNETES DELIVERY</p>
<h1>CloudOps Autopilot</h1>
<p class="status">Deployed automatically by GitHub Actions.</p>
<ul>
<li>Container image: ready</li>
<li>Kubernetes deployment: running</li>
<li>Health endpoint: ready</li>
</ul>
</main>
</body>
</html>
What should match?
The page structure and styles remain unchanged. The status paragraph now contains the automated deployment message.
The workflow lives under .github/workflows. GitHub reads YAML files in this folder when repository events occur.
- In the Visual Studio Code Explorer sidebar, create a .github folder inside cloudops-autopilot with the New Folder icon.
- Create a workflows folder inside .github with the New Folder icon.
The Explorer now shows the nested .github/workflows path inside cloudops-autopilot.
- Create deploy.yml inside .github/workflows with the New File icon.
- Add the workflow trigger and runner configuration by pasting this code into deploy.yml:
name: Build Test and Deploy
on:
push:
branches:
- main
permissions:
contents: read
concurrency:
group: cloudops-autopilot-local
cancel-in-progress: true
jobs:
deploy:
runs-on: [self-hosted, windows, x64]
defaults:
run:
shell: powershell
What does this configuration do?
- The workflow is named Build Test and Deploy.
- The push trigger limits runs to the main branch.
- The permissions section grants read access to repository contents.
- The concurrency group cancels an older run when a newer deployment starts.
- The runner labels target your repository-level Windows x64 machine.
- PowerShell executes every command in the deployment job.
- Save deploy.yml.
The saved file now defines when the job starts and which runner receives it.
YAML indentation looks wrong?
Keep each nested section aligned exactly as shown. YAML uses spaces to define which values belong together.
Ask for help with checking the first workflow section.
- Place your cursor after the shell: powershell line.
- Add the workflow steps with this code:
steps:
- name: Check out repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
- name: Verify local platform
run: |
docker info --format '{{.OSType}}'
minikube status
- name: Build image
run: docker build --tag cloudops-autopilot:local .
- name: Test NGINX configuration
run: docker run --rm cloudops-autopilot:local nginx -t
- name: Load image into Minikube
run: minikube image load cloudops-autopilot:local --daemon --overwrite
- name: Deploy and wait
run: |
minikube kubectl -- apply -f k8s/deployment.yaml
minikube kubectl -- rollout restart deployment/cloudops-autopilot
minikube kubectl -- rollout status deployment/cloudops-autopilot --timeout=120s
How does the delivery job work?
- The checkout step pins actions/checkout release v7.0.1 to its full commit SHA.
- The platform check confirms that Docker uses Linux containers.
- The platform check also confirms that Minikube is running.
- The build step creates the updated local image.
- The test step checks the NGINX configuration.
- The load step replaces the image stored in Minikube.
- The final step applies the manifest.
- The final step also restarts the Deployment before waiting for rollout success.
- Save deploy.yml.
The final line now waits up to 120s for the Deployment rollout to finish.
Workflow steps not aligned?
Make sure steps aligns with runs-on inside the deploy job. Each step name starts at the same indentation level.
Ask for help with fixing the workflow indentation.
✔️ Awesome, I've got everything!
Your saved workflow now covers the complete local build and deployment sequence.
ⓧ I'd like to double check the full code
- Compare your complete .github/workflows/deploy.yml with this file:
name: Build Test and Deploy
on:
push:
branches:
- main
permissions:
contents: read
concurrency:
group: cloudops-autopilot-local
cancel-in-progress: true
jobs:
deploy:
runs-on: [self-hosted, windows, x64]
defaults:
run:
shell: powershell
steps:
- name: Check out repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
- name: Verify local platform
run: |
docker info --format '{{.OSType}}'
minikube status
- name: Build image
run: docker build --tag cloudops-autopilot:local .
- name: Test NGINX configuration
run: docker run --rm cloudops-autopilot:local nginx -t
- name: Load image into Minikube
run: minikube image load cloudops-autopilot:local --daemon --overwrite
- name: Deploy and wait
run: |
minikube kubectl -- apply -f k8s/deployment.yaml
minikube kubectl -- rollout restart deployment/cloudops-autopilot
minikube kubectl -- rollout status deployment/cloudops-autopilot --timeout=120s
What should match?
The complete file contains one push trigger and one deployment job. The job checks out the repository before it verifies, builds, tests, loads, deploys, and waits.
Push the project and verify the rollout
Git records the complete project as a commit. Pushing that commit to main becomes the trigger for your delivery workflow.
- Switch back to the PowerShell terminal inside your cloudops-autopilot folder.
- Initialize the repository and create its first commit by running these commands:
git init -b main
git add .
git commit -m "Build CloudOps Autopilot"
What do these commands do?
- The first line initializes the repository on a branch named main.
- The second line stages every project file.
- The third line records the staged files in a commit named Build CloudOps Autopilot.
PowerShell prints a commit summary that includes the files added to the repository.
Commit not created?
Confirm the terminal path ends with cloudops-autopilot. A different folder would initialize the wrong repository.
Ask for help with creating the first commit.
- Copy the repository URL shown by GitHub.
- Record the copied URL here: REMOTE-URL.
- Connect the local repository to GitHub by running this command:
git remote add origin [[REMOTE_URL="REMOTE-URL"]]
What does this command do?
The command stores your private repository URL under the remote name origin. Git uses that remote when you push the main branch.
PowerShell returns to the prompt when the remote is added successfully.
Remote already exists?
This usually means the folder already has an origin remote. Check that you are using the newly initialized cloudops-autopilot repository.
Ask for help with connecting the repository remote.
Before you push, do you expect the runner terminal to stay idle or begin processing the deployment workflow?
- Push the main branch by running this command:
git push -u origin main
What does this command trigger?
The push uploads your commit and creates the main branch in the private repository. The workflow then assigns its deployment job to your online runner.
The first run can take a few minutes while the runner builds the image and performs the rollout.
- In the GitHub repository navigation, select Actions.
- Open the Build Test and Deploy workflow run.
- Watch the deploy job until it succeeds.
Great work. Every workflow step now shows success, which confirms your runner rebuilt the image and deployed it to Minikube.
Workflow queued or failed?
- Check that the runner terminal still displays Listening for Jobs if the job remains queued.
- Confirm the runner has the self-hosted, windows, and x64 labels.
- Open the first failed workflow step to identify which local command needs attention.
Ask for help with debugging the workflow run.
Before you check the cluster, do you expect the Deployment to report a completed rollout after the successful workflow?
- Switch back to the project PowerShell terminal from earlier.
- Verify the rollout by running this command:
minikube kubectl -- rollout status deployment/cloudops-autopilot --timeout=120s
What does this check prove?
The command waits for the latest cloudops-autopilot rollout to finish. A successful result confirms the new Pod became available.
You will see the rollout report success before the timeout expires.
- Switch to a separate PowerShell terminal so the runner can remain active.
- Reopen the Service port-forward by running this command:
minikube kubectl -- port-forward service/cloudops-autopilot 8080:80
What does the port-forward do?
The command routes local port 8080 to port 80 on the Kubernetes Service. Keep this terminal open while you check the page.
Press Ctrl+C when you want to end the forwarding session.
- Open http://localhost:8080 in your browser.
You will see the CloudOps Autopilot page with Deployed automatically by GitHub Actions. in the status panel.
Still seeing the old status?
Confirm the workflow's build step ran after you committed the updated index.html file.
Restart the port-forward if its selected Pod ended during the Deployment rollout.
Ask for help with finding the deployed page version.
That completes your delivery loop. A push now builds, tests, loads, deploys, and verifies CloudOps Autopilot on your local Kubernetes cluster.
Secret mission
Add a Readiness Gate
An unhealthy Pod should stop receiving Service traffic before Kubernetes restarts its container. Add a readiness probe through your existing delivery workflow, then watch the Pod leave traffic and recover.
Clean Up Your Resources
Clean Up Your Resources
Choose whether to keep your resources running, pause them for later, or delete them entirely. Self-hosted GitHub Actions is free to use. Your computer provides the compute and storage.
Resources you used:
- A local Minikube cluster running on your computer.
- A Kubernetes Deployment named cloudops-autopilot.
- A Kubernetes Service named cloudops-autopilot.
- A local Docker image named cloudops-autopilot:local.
- A private GitHub repository named cloudops-autopilot.
- A repository-level Windows x64 self-hosted runner registration.
Keep everything running
No action needed. Choose this if you are still testing the delivery loop or planning more pushes to main.
- Your Minikube cluster keeps the readiness-enabled Deployment available.
- Your Windows x64 self-hosted runner continues to display Listening for Jobs.
- Your private repository preserves the project source and workflow history.
- Your local Docker image remains ready for another rollout.
Pause - I'll come back to this later
Shut down the active runner process and local cluster while keeping every registration, image, manifest, and repository for later.
- Stop the runner application by pressing Ctrl+C in its PowerShell terminal.
- Pause the local Minikube cluster by running this command:
minikube stop
What does this pause?
The stop command halts the local cluster while preserving its configuration. The Deployment and Service remain available when you start the cluster again.
- Return to the private cloudops-autopilot repository.
- Select Settings.
- Select Actions.
- Select Runners.
- Confirm that the runner remains registered as offline.
Delete - I don't want to use this again
Remove every listed project resource from its current location. Deletion permanently removes the cluster state and repository history.
Remove the runner registration
- Return to the private cloudops-autopilot repository.
- Select Settings.
- Select Actions.
- Select Runners.
- Select your Windows x64 runner.
- Choose Remove.
- Run GitHub's generated removal command in the runner PowerShell terminal from earlier.
Delete the Minikube cluster
- Delete every local Minikube cluster by running this command in PowerShell:
minikube delete --all
What does this command remove?
This command removes every local Minikube cluster. The cloudops-autopilot Deployment and Service disappear with their cluster.
Remove the Docker image
- Remove the local CloudOps Autopilot image by running this command in PowerShell:
docker image rm cloudops-autopilot:local
What does the image command remove?
This removes the local cloudops-autopilot:local image from Docker Desktop.
Delete the private repository
- Return to the private cloudops-autopilot repository.
- Open the repository's Settings.
- Use the repository deletion control to delete cloudops-autopilot.
- Complete the confirmation shown by GitHub to remove the source and workflow history.
Nice Work!
Nice Work!
You made it! CloudOps Autopilot now demonstrates a push-triggered local Kubernetes rollout that protects traffic before recovering an unhealthy container.
You've learned how to:
- Build a pinned Docker image for the CloudOps Autopilot page. Validate its NGINX configuration. Load the same image into Minikube without a container registry.
- Deploy the status page through a Kubernetes Deployment. Route local browser traffic through its Service. Prove the liveness probe restarts a live-but-unhealthy container after /health fails.
- Connect a private GitHub repository to a Windows self-hosted runner. Use GitHub Actions to turn each push to main into a verified Kubernetes deployment.
- Secret Mission: Add a readiness probe that removes an unhealthy Pod from Service traffic before the liveness restart. Verify readiness falls to 0/1 before recovery returns it to 1/1.
Ready to quiz yourself?