Docker and Kubernetes ServicePulse

Containerize a status dashboard and run it with Kubernetes.

Introduction

30 Second Summary

One running app can look reliable right up until its only process stops. A strong infrastructure demo shows what happens next.

In this project, you will build ServicePulse as a static operations dashboard packaged with Docker. You will then prove that Kubernetes restores the dashboard's desired state after a Pod disappears.

What You'll Build

Your finished browser demo shows three operational services under a clear release version while Kubernetes keeps three copies of the dashboard running.

By the end of this project, you'll have:

  • A browser dashboard showing three named services as operational under a visible application version.
  • A self-healing demonstration that returns to three running Pods after one is deleted.
  • A public GitHub repository that lets a recruiter reproduce the demo from your README.
  • Secret Mission: Add HTTP health probes plus explicit CPU and memory guardrails to the Deployment.

Are there any prerequisites?

You need Docker Desktop connected to Ubuntu through WSL 2.

You also need Visual Studio Code plus a GitHub account.

Before We Start

Start by locking in the story your ServicePulse demo will tell before the hands-on work begins. You will show Docker packaging a static nginx dashboard while Kubernetes maintains the dashboard's availability, scale, recovery, and updates.

Prepare the WSL 2 Toolchain

Your ServicePulse demo starts with Docker packaging the static dashboard. Kubernetes later keeps that dashboard available.

Ubuntu must reach Docker Desktop through WSL 2. It also needs the Docker Desktop Kubernetes context.

A pre-flight check catches missing tools before any files exist. You will verify Git plus the Docker and kubectl command paths before creating the empty workspace.

In this step, get ready to:
  • Configure Docker Desktop for Ubuntu through WSL 2.
  • Create a single-node Kubeadm cluster and verify the command path.
  • Create the Linux-side ServicePulse workspace.
Configure Docker Desktop and Kubernetes

A local cluster gives Kubernetes a control plane plus a node for running your future Pods. Kubeadm keeps this cluster to one node so the demo stays conservative with memory.

  • Press the Windows key to open Windows search.
  • Type Docker Desktop into Windows search. Press Enter to open it.
  • Select Settings using the gear icon in Docker Desktop.
  • Select General in the settings navigation.
  • Turn on Use WSL 2 based engine if it is disabled.
  • Select Resources in the settings navigation.
  • Select WSL Integration under Resources.
  • Turn on integration for Ubuntu if it is disabled.

What does WSL integration do?

WSL integration exposes Docker commands inside the selected Ubuntu distribution. Your Linux terminal can then use the engine managed by Docker Desktop.

Cluster creation can sit in progress while Docker Desktop prepares the control plane. That waiting is expected.

  • Save the Docker Desktop settings using the confirmation control at the bottom of the window.
  • Return to the main Docker Desktop window.
  • Select the Kubernetes view in the left navigation.
  • Click Create cluster.
  • Choose Kubeadm as the provisioner.
  • Confirm the cluster creation.
  • Wait until Docker Desktop shows the cluster as running.

Why Kubeadm for this cluster?

Kubeadm creates a single-node Docker Desktop cluster. It works with both supported Docker Desktop image stores.

Docker Desktop's kind option supports multiple nodes. Its image path adds complexity because it does not work with the Docker image store.

The slowest setup is complete. Docker Desktop now has a local Kubernetes cluster ready for the ServicePulse workload.

Verify the Ubuntu command path

WSL can launch Windows executables from Ubuntu when their names include the .exe extension. This lets Ubuntu use the kubectl.exe bundled with Docker Desktop.

  • Press the Windows key to open Windows search.
  • Type Ubuntu into Windows search. Press Enter to open it.
  • Check Docker access plus the Git and kubectl command paths by running:
docker version
git --version
kubectl.exe version --client

What do these checks prove?

  • The first command asks Docker Desktop for client and server details.
  • The second command confirms that Git is installed inside Ubuntu.
  • The third command proves that WSL can reach Docker Desktop's Windows kubectl executable.

✔️ All three commands returned details

Your command path is ready. Ubuntu can reach Docker plus the tools needed to manage the project.

ⓧ Git is not installed

A fresh Ubuntu distribution may not include Git. If Ubuntu asks for your password during installation, type it even though the terminal does not display the characters.

  • Install Git from Ubuntu's package repositories by running:
sudo apt install git-all

What does this command do?

The command installs Git plus its complete Ubuntu package set. Ubuntu may ask you to confirm the package changes before continuing.

  • Verify the Git installation by running:
git --version

What should I see?

Git prints its installed version. That output confirms Ubuntu can run the command.

Git installation still failing?

Check that Ubuntu has an internet connection. Then use this prompt for targeted help: Help me diagnose why Git will not install in Ubuntu on WSL 2.

ⓧ kubectl.exe is unavailable

This result usually means Ubuntu cannot reach Docker Desktop's Windows command path. Recheck the integration before testing again.

  • Switch back to Docker Desktop.
  • Return to WSL Integration under Resources.
  • Turn on integration for Ubuntu.
  • Save the Docker Desktop settings.
  • Close the current Ubuntu window.
  • Press the Windows key to open Windows search.
  • Type Ubuntu into Windows search. Press Enter to reopen it.
  • Test the Windows executable path again by running:
kubectl.exe version --client

What should I see?

The command prints kubectl client details. That output proves WSL interoperability can reach the executable supplied by Docker Desktop.

kubectl.exe still unavailable?

Confirm Docker Desktop is running before reopening Ubuntu. Then use this prompt for targeted help: Help me restore kubectl.exe access from Ubuntu.

Docker unavailable in Ubuntu?

Switch back to Docker Desktop. Confirm the WSL 2 engine is enabled plus Ubuntu integration is turned on.

Reopen Ubuntu after saving those settings. If the check still fails, use this prompt: Help me restore Docker access in Ubuntu.

A persistent Bash alias lets you type kubectl while Ubuntu runs kubectl.exe. The ~/.bashrc file loads this shortcut for future shell sessions.

  • Open your Bash configuration file from your Ubuntu home directory by running:
cd ~
notepad.exe .bashrc

What do these commands do?

The first command moves the terminal to your Ubuntu home directory. The second command opens its Bash configuration file in Windows Notepad.

  • Scroll to the bottom of the file in Notepad.
  • Add the kubectl shortcut on a new line by copying:
alias kubectl='kubectl.exe'

What does this alias do?

The alias maps the shorter kubectl command to Docker Desktop's Windows executable. Your later Kubernetes commands can use standard kubectl syntax.

  • Save .bashrc in Notepad.
  • Close Notepad.
  • Load the alias and select Docker Desktop's Kubernetes context by running:
source ~/.bashrc
kubectl version --client
kubectl config use-context docker-desktop

What do these commands do?

  • The first command reloads your Bash configuration in the current Ubuntu session.
  • The second command proves the new alias reaches the kubectl client.
  • The third command selects Docker Desktop's local Kubernetes cluster.

You should see kubectl client details. The context command also confirms that Docker Desktop is selected.

Create the Linux workspace

Linux-side storage keeps this command-line project inside the WSL filesystem. Visual Studio Code can open the same folder through its WSL connection.

  • Create the ServicePulse workspace plus its empty subdirectories by running:
mkdir -p ~/servicepulse/site ~/servicepulse/k8s
cd ~/servicepulse

What do these commands create?

  • The first command creates the ~/servicepulse workspace.
  • It also creates empty site and k8s directories inside that workspace.
  • The second command moves your terminal into the new workspace.

The first WSL launch may fetch supporting components for VS Code. Let that setup finish before checking the Explorer sidebar.

  • Open the current WSL workspace in VS Code by running:
code .

What does this command do?

The period tells VS Code to open the current directory. Because the command runs inside Ubuntu, VS Code opens ~/servicepulse through WSL.

In the VS Code Explorer sidebar, you should see the empty site and k8s directories. No project code files exist yet.

VS Code does not open?

Close Ubuntu before reopening it from Windows search. This refreshes the command path used by your existing VS Code installation.

If the command remains unavailable, use this prompt: Help me open my WSL workspace in VS Code.

Before you run the final check, ask yourself whether every command will still succeed from inside the new workspace.

  • Prove the full Ubuntu toolchain can reach Docker Desktop and its cluster by running:
docker version
kubectl version --client
git --version
kubectl cluster-info

What confirms success?

  • Docker prints client and server component details.
  • kubectl prints its client details through your Bash alias.
  • Git prints its installed version.
  • The cluster check prints control-plane and cluster-service information without a connection error.

Your Ubuntu terminal can now reach every tool required by ServicePulse. The empty workspace is also open in VS Code.

Your local toolchain is connected from editor to cluster. Next up, you will build the first visible ServicePulse container and expose its availability gap.

Build and Break the Standalone Container

Your Ubuntu toolchain can now reach Docker from the ~/servicepulse workspace. The next goal is to turn that empty workspace into a dashboard you can run.

You will package ServicePulse as a versioned image. Then you will test how one standalone container behaves when its process is stopped.

In this step, get ready to:
  • Create the ServicePulse v1 dashboard files.
  • Build the servicepulse:v1 image.
  • Run a standalone container through a stop test.
Shape the ServicePulse dashboard

The dashboard needs visible content before Docker can package it. Its HTML provides the structure while its CSS turns that structure into an operations display.

  • Create site/index.html from the VS Code Explorer sidebar.
  • Add the page shell by pasting this first section into site/index.html:
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>ServicePulse</title>
  <link rel="stylesheet" href="styles.css">
</head>
<body>
  <main class="container">
    <header class="hero">
      <p class="eyebrow">Containerized operations demo</p>
      <div class="title-row">
        <h1>ServicePulse</h1>
        <span class="version">Version 1</span>
      </div>
      <p class="subtitle">A static status dashboard packaged with Docker and orchestrated by Kubernetes.</p>
    </header>

What does this section do?

  • The document head sets the browser title plus the mobile viewport.
  • The stylesheet link connects the page to styles.css.
  • The hero introduces ServicePulse as Version 1.
  • Add the service cards below the closing </header> tag by pasting this section:

    <section class="status-grid" aria-label="Service status">
      <article class="status-card">
        <span class="indicator" aria-hidden="true"></span>
        <div>
          <h2>API Gateway</h2>
          <p>Operational</p>
        </div>
      </article>
      <article class="status-card">
        <span class="indicator" aria-hidden="true"></span>
        <div>
          <h2>Orders Service</h2>
          <p>Operational</p>
        </div>
      </article>
      <article class="status-card">
        <span class="indicator" aria-hidden="true"></span>
        <div>
          <h2>Notifications</h2>
          <p>Operational</p>
        </div>
      </article>
    </section>

How are the services represented?

Each status card pairs a green indicator with a service name. The repeated structure keeps all three services visually consistent.

The status grid groups API Gateway, Orders Service, and Notifications into one dashboard region.

  • Finish site/index.html by adding this final section:

    <section class="panel">
      <h2>Deployment story</h2>
      <ul>
        <li>Packaged as versioned Docker images</li>
        <li>Exposed through a stable Kubernetes Service</li>
        <li>Scaled and restored through declared desired state</li>
      </ul>
    </section>

    <footer>Served by nginx inside Kubernetes</footer>
  </main>
</body>
</html>

What completes the page?

The panel previews the deployment story that this project demonstrates. The footer identifies nginx as the server inside the final workload.

The closing tags complete the main content plus the HTML document.

  • Save site/index.html.
  • Confirm that the file ends with the closing </html> tag.

Does the HTML structure look incomplete?

Check that all three snippets appear in the same site/index.html file. Make sure the status grid sits between the hero and panel sections.

Use this prompt if a tag looks misplaced: Help me compare the structure of my ServicePulse HTML with the provided snippets.

The HTML now defines every dashboard region. The stylesheet supplies the layout, colours, spacing, and responsive card grid.

  • Create site/styles.css from the VS Code Explorer sidebar.
  • Set the page foundation by pasting this first section into site/styles.css:
:root {
  color-scheme: dark;
  font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  background: #07111f;
  color: #e8f0ff;
}

* {
  box-sizing: border-box;
}

body {
  margin: 0;
  min-height: 100vh;
  background:
    radial-gradient(circle at top right, rgba(34, 197, 94, 0.18), transparent 32rem),
    linear-gradient(160deg, #07111f 0%, #0d1b2e 100%);
}

What does the foundation control?

The root styles establish the dark colour palette plus the font stack. The universal selector makes element dimensions include their borders and padding.

The body gradient creates the blue background with a green glow near the top.

  • Add the dashboard frame below the body block by pasting:

.container {
  width: min(960px, calc(100% - 2rem));
  margin: 0 auto;
  padding: 4rem 0 2rem;
}

.hero,
.panel,
.status-card {
  border: 1px solid rgba(148, 163, 184, 0.2);
  background: rgba(15, 23, 42, 0.78);
  box-shadow: 0 24px 70px rgba(0, 0, 0, 0.25);
}

.hero {
  padding: 2rem;
  border-radius: 1.5rem;
}

How does the frame work?

The container keeps the dashboard centred within a readable width. The shared card styles give the hero, panel, and service cards one visual language.

  • Add the labels plus title layout below the .hero block by pasting:

.eyebrow,
.version,
.release {
  color: #86efac;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

.eyebrow,
.release {
  font-size: 0.78rem;
}

.title-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  flex-wrap: wrap;
}

Why share these label styles?

The eyebrow and version badge use the same green accent. The release selector prepares the stylesheet for the v2 release label without displaying that label in v1.

The title row keeps the heading plus version badge aligned while allowing them to wrap on narrow screens.

  • Style the main title plus subtitle by adding:

h1 {
  margin: 0.4rem 0;
  font-size: clamp(2.5rem, 8vw, 5rem);
  line-height: 1;
}

.version {
  padding: 0.55rem 0.8rem;
  border: 1px solid rgba(134, 239, 172, 0.45);
  border-radius: 999px;
  font-size: 0.75rem;
}

.subtitle {
  max-width: 680px;
  color: #b8c6da;
  font-size: 1.05rem;
  line-height: 1.7;
}

What changes in the hero?

The heading scales with the browser width. The version text becomes a compact badge while the subtitle stays readable across a wide line.

  • Build the responsive service grid by adding:

.status-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: 1rem;
  margin: 1rem 0;
}

.status-card {
  display: flex;
  align-items: center;
  gap: 1rem;
  padding: 1.25rem;
  border-radius: 1rem;
}

.status-card h2,
.panel h2 {
  margin: 0 0 0.35rem;
}

.status-card p {
  margin: 0;
  color: #86efac;
}

How does the grid respond?

The grid creates as many card columns as the available width supports. Each card aligns its indicator beside its service details.

The minimum card width keeps the service names readable before the layout wraps.

  • Finish site/styles.css by adding the indicator, panel, and footer styles:

.indicator {
  width: 0.8rem;
  height: 0.8rem;
  flex: 0 0 auto;
  border-radius: 50%;
  background: #22c55e;
  box-shadow: 0 0 1rem rgba(34, 197, 94, 0.9);
}

.panel {
  padding: 1.5rem 2rem;
  border-radius: 1rem;
}

.panel li {
  margin: 0.65rem 0;
  color: #cbd5e1;
}

footer {
  padding: 1.5rem 0;
  color: #7890aa;
  text-align: center;
  font-size: 0.9rem;
}

What finishes the visual system?

The indicator becomes a glowing green status light. The panel spacing separates the deployment story from the service cards.

The muted footer closes the page without competing with the dashboard.

  • Save site/styles.css.
  • Confirm that the file ends with the footer selector.

Seeing CSS warnings in VS Code?

Check that every selector has matching braces. Confirm that each snippet appears once in the same site/styles.css file.

Use this prompt to trace the problem: Help me find an unmatched brace or misplaced CSS declaration in my ServicePulse stylesheet.

A base image supplies the runtime plus the server that receives browser requests. This project uses the official nginx image tagged nginx:1.30.6-alpine-slim.

  • Create Dockerfile in the top level of ~/servicepulse from the VS Code Explorer sidebar.
  • Define the image recipe by pasting:
FROM nginx:1.30.6-alpine-slim

COPY site/ /usr/share/nginx/html/

EXPOSE 80

What does the Dockerfile do?

  • The FROM instruction selects the pinned nginx base image.
  • The COPY instruction places the dashboard files in nginx's static content directory.
  • The EXPOSE instruction documents port 80 as the container's web port.
  • Save Dockerfile.
  • Create .dockerignore in the top level of ~/servicepulse from the VS Code Explorer sidebar.
  • Define the excluded build files by pasting:
.git
k8s
README.md

Why exclude these paths?

The ignore file keeps source-control data, Kubernetes manifests, and project documentation outside the image build input. The build receives only the files needed for the dashboard image.

  • Save .dockerignore.
  • Confirm that the VS Code Explorer lists Dockerfile plus .dockerignore above the site folder.

Are the packaging files in the wrong place?

Make sure Dockerfile plus .dockerignore sit directly inside ~/servicepulse. Keep index.html plus styles.css inside its site subfolder.

Use this prompt if the file tree differs: Help me correct my ServicePulse file locations before I build the Docker image.

✔️ Awesome, I've got everything!

Your four project files are ready for the first image build.

ⓧ I'd like to double check the full code

  • Compare each saved file with the matching reference below.
.git
k8s
README.md
FROM nginx:1.30.6-alpine-slim

COPY site/ /usr/share/nginx/html/

EXPOSE 80
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>ServicePulse</title>
  <link rel="stylesheet" href="styles.css">
</head>
<body>
  <main class="container">
    <header class="hero">
      <p class="eyebrow">Containerized operations demo</p>
      <div class="title-row">
        <h1>ServicePulse</h1>
        <span class="version">Version 1</span>
      </div>
      <p class="subtitle">A static status dashboard packaged with Docker and orchestrated by Kubernetes.</p>
    </header>

    <section class="status-grid" aria-label="Service status">
      <article class="status-card">
        <span class="indicator" aria-hidden="true"></span>
        <div>
          <h2>API Gateway</h2>
          <p>Operational</p>
        </div>
      </article>
      <article class="status-card">
        <span class="indicator" aria-hidden="true"></span>
        <div>
          <h2>Orders Service</h2>
          <p>Operational</p>
        </div>
      </article>
      <article class="status-card">
        <span class="indicator" aria-hidden="true"></span>
        <div>
          <h2>Notifications</h2>
          <p>Operational</p>
        </div>
      </article>
    </section>

    <section class="panel">
      <h2>Deployment story</h2>
      <ul>
        <li>Packaged as versioned Docker images</li>
        <li>Exposed through a stable Kubernetes Service</li>
        <li>Scaled and restored through declared desired state</li>
      </ul>
    </section>

    <footer>Served by nginx inside Kubernetes</footer>
  </main>
</body>
</html>
:root {
  color-scheme: dark;
  font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  background: #07111f;
  color: #e8f0ff;
}

* {
  box-sizing: border-box;
}

body {
  margin: 0;
  min-height: 100vh;
  background:
    radial-gradient(circle at top right, rgba(34, 197, 94, 0.18), transparent 32rem),
    linear-gradient(160deg, #07111f 0%, #0d1b2e 100%);
}

.container {
  width: min(960px, calc(100% - 2rem));
  margin: 0 auto;
  padding: 4rem 0 2rem;
}

.hero,
.panel,
.status-card {
  border: 1px solid rgba(148, 163, 184, 0.2);
  background: rgba(15, 23, 42, 0.78);
  box-shadow: 0 24px 70px rgba(0, 0, 0, 0.25);
}

.hero {
  padding: 2rem;
  border-radius: 1.5rem;
}

.eyebrow,
.version,
.release {
  color: #86efac;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

.eyebrow,
.release {
  font-size: 0.78rem;
}

.title-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  flex-wrap: wrap;
}

h1 {
  margin: 0.4rem 0;
  font-size: clamp(2.5rem, 8vw, 5rem);
  line-height: 1;
}

.version {
  padding: 0.55rem 0.8rem;
  border: 1px solid rgba(134, 239, 172, 0.45);
  border-radius: 999px;
  font-size: 0.75rem;
}

.subtitle {
  max-width: 680px;
  color: #b8c6da;
  font-size: 1.05rem;
  line-height: 1.7;
}

.status-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: 1rem;
  margin: 1rem 0;
}

.status-card {
  display: flex;
  align-items: center;
  gap: 1rem;
  padding: 1.25rem;
  border-radius: 1rem;
}

.status-card h2,
.panel h2 {
  margin: 0 0 0.35rem;
}

.status-card p {
  margin: 0;
  color: #86efac;
}

.indicator {
  width: 0.8rem;
  height: 0.8rem;
  flex: 0 0 auto;
  border-radius: 50%;
  background: #22c55e;
  box-shadow: 0 0 1rem rgba(34, 197, 94, 0.9);
}

.panel {
  padding: 1.5rem 2rem;
  border-radius: 1rem;
}

.panel li {
  margin: 0.65rem 0;
  color: #cbd5e1;
}

footer {
  padding: 1.5rem 0;
  color: #7890aa;
  text-align: center;
  font-size: 0.9rem;
}
Run ServicePulse locally

A build context is the collection of files Docker can read during a build. The final dot in the build command selects the current ~/servicepulse directory as that context.

  • Build the immutable servicepulse:v1 image by running this command in the Ubuntu terminal from earlier:
docker build -t servicepulse:v1 .

What does this command do?

  • The build command reads Dockerfile from the selected context.
  • The tag records this release as servicepulse:v1.
  • The image stores the dashboard plus its pinned nginx runtime as one reusable package.

Docker prints the build steps in the terminal. A completed build confirms that the v1 image is available locally.

Did the image build fail?

Confirm that the terminal is currently inside ~/servicepulse. Check that Dockerfile uses a capital D.

Use this prompt if Docker stops during a build step: Help me diagnose why my ServicePulse Docker image build failed.

A port mapping connects a port on your computer to a port inside the container. The mapping 127.0.0.1:8080:80 sends local browser traffic on port 8080 to nginx on container port 80.

  • Run the image as servicepulse-local by entering this command:
docker run --name servicepulse-local -d -p 127.0.0.1:8080:80 servicepulse:v1

What does this command do?

  • The container name makes this running instance easy to target later.
  • Detached mode keeps the container running in the background.
  • The loopback port mapping limits access to your local computer.
  • Enter http://localhost:8080 in your Windows browser.

You should see the dark ServicePulse dashboard with three operational services. The hero should display Version 1.

That is the first working release. Your versioned image now serves a visible dashboard from one running container.

Does the dashboard fail to load?

Confirm that Docker Desktop is still running. Check whether another local process is already using port 8080.

Use this prompt for focused help: Help me find why my ServicePulse container is not reachable on localhost port 8080.

Test the single-container boundary

The dashboard currently depends on one container process. A deliberate stop test shows what that dependency means for someone trying to reach the page.

Before you run the stop command, do you think the dashboard will remain reachable?

  • Stop servicepulse-local by running this command:
docker stop servicepulse-local

What does this command do?

The stop command ends the process inside the named container. The container remains on disk in a stopped state.

  • Refresh http://localhost:8080 in your browser.

The ServicePulse dashboard no longer loads. That outage is the intended result of this test.

The image still exists. The stopped container has no controller keeping the dashboard available.

You now have a packaged dashboard plus direct evidence of its availability gap. Next up, you will use Kubernetes to declare how many running copies should exist.

Deploy ServicePulse to Kubernetes

Your versioned image works, but stopping its lone Docker container took the dashboard offline. That outage exposed the desired state gap this step solves.

Kubernetes closes that gap through a Deployment. The Deployment asks a ReplicaSet to maintain a declared number of Pods. A Service gives those replaceable Pods one stable network target.

In this step, get ready to:
  • Declare two ServicePulse Pods with a Deployment.
  • Expose the Pods through a stable ClusterIP Service.
  • Reach the Version 1 dashboard from your browser.
Declare the workload with a Deployment

A YAML manifest records the state Kubernetes should maintain. This one requests two copies of ServicePulse from the local servicepulse:v1 image.

  • Switch back to the existing servicepulse workspace in Visual Studio Code.
  • Select the k8s folder in the Explorer sidebar.
  • Create a new file named deployment.yaml inside the k8s folder.
  • Paste this manifest into k8s/deployment.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
  name: servicepulse
  labels:
    app: servicepulse
spec:
  replicas: 2
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxUnavailable: 1
      maxSurge: 1
  selector:
    matchLabels:
      app: servicepulse
  template:
    metadata:
      labels:
        app: servicepulse
    spec:
      containers:
        - name: servicepulse
          image: servicepulse:v1
          imagePullPolicy: IfNotPresent
          ports:
            - name: http
              containerPort: 80

How Does This Deployment Work?

  • The replicas: 2 setting declares that two Pods should exist.
  • The app: servicepulse label identifies each managed Pod.
  • The matching selector connects the Deployment to those labeled Pods.
  • The RollingUpdate strategy controls how future versions replace existing Pods.
  • The servicepulse:v1 tag identifies the exact image release.
  • The IfNotPresent policy uses the local image when it already exists.
  • The named http port gives the Service a stable port name to target.
  • Save k8s/deployment.yaml.
  • Confirm that deployment.yaml appears inside the k8s folder in the Explorer sidebar.

Does the Manifest Look Misaligned?

  • Compare each indentation level with the reference below.
  • Replace any tab characters with spaces.
  • Check that containers sits inside the Pod template's spec section.

Help me check the indentation in my ServicePulse Deployment manifest.

✔️ Awesome, I've got everything!

Your Deployment manifest now matches the required two-Pod configuration.

ⓧ I'd like to double check the full code

apiVersion: apps/v1
kind: Deployment
metadata:
  name: servicepulse
  labels:
    app: servicepulse
spec:
  replicas: 2
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxUnavailable: 1
      maxSurge: 1
  selector:
    matchLabels:
      app: servicepulse
  template:
    metadata:
      labels:
        app: servicepulse
    spec:
      containers:
        - name: servicepulse
          image: servicepulse:v1
          imagePullPolicy: IfNotPresent
          ports:
            - name: http
              containerPort: 80
Give the Pods a stable Service

Individual Pod addresses can change whenever Kubernetes replaces a Pod. A ClusterIP Service selects the ServicePulse Pods through their shared label.

The Service becomes the stable target for traffic inside the cluster. Its selector keeps routing requests to the matching Pods as they change.

  • Select the k8s folder in the Explorer sidebar.
  • Create a new file named service.yaml inside the k8s folder.
  • Paste this manifest into k8s/service.yaml:
apiVersion: v1
kind: Service
metadata:
  name: servicepulse
spec:
  type: ClusterIP
  selector:
    app: servicepulse
  ports:
    - name: http
      protocol: TCP
      port: 80
      targetPort: http

How Does This Service Work?

  • The ClusterIP type exposes ServicePulse through a stable address inside the cluster.
  • The app: servicepulse selector finds the Pods created by the Deployment.
  • The Service accepts traffic on port 80.
  • The targetPort: http setting sends that traffic to the container port named http.
  • Save k8s/service.yaml.
  • Confirm that service.yaml appears beside deployment.yaml in the Explorer sidebar.

Does the Service Selector Match?

  • Compare the Service selector with the Deployment's Pod label.
  • Confirm that both values use app: servicepulse.
  • Check that targetPort uses the container port name http.

Help me compare my Service selector with my Deployment labels.

✔️ Awesome, I've got everything!

Your Service manifest now targets the Pods declared by the Deployment.

ⓧ I'd like to double check the full code

apiVersion: v1
kind: Service
metadata:
  name: servicepulse
spec:
  type: ClusterIP
  selector:
    app: servicepulse
  ports:
    - name: http
      protocol: TCP
      port: 80
      targetPort: http
Apply the manifests and open ServicePulse

The two manifests now describe the workload plus its stable network target. Sending their contents through standard input lets kubectl create both resources in the connected cluster.

Before you apply them, consider whether Kubernetes creates one Pod or the two declared replicas.

  • Apply both manifests from the servicepulse workspace by running these commands:
cat k8s/deployment.yaml | kubectl apply -f -
cat k8s/service.yaml | kubectl apply -f -
kubectl rollout status deployment/servicepulse

What Do These Commands Do?

  • The first pipeline sends the Deployment manifest directly to Kubernetes.
  • The second pipeline creates the Service from its manifest.
  • The rollout check waits for the Deployment to reach its declared state.

The rollout check finishes when both ServicePulse replicas are available.

  • Inspect the managed Pods by running this command:
kubectl get pods -l app=servicepulse

What Should You See?

You should see two ServicePulse Pod rows. Each Pod should show a Running status.

Those generated Pod names show that the Deployment manages replaceable instances through its ReplicaSet.

Is the Rollout Still Waiting?

  • Confirm that the manifest uses the locally built servicepulse:v1 image tag.
  • Confirm that the current cluster context is docker-desktop.
  • Check the Pod list for a status mentioning an unavailable image.

Help me diagnose why my ServicePulse Deployment is not reaching two running Pods.

That is the availability gap closed. Kubernetes is now maintaining two running Pods from your local image.

The ClusterIP Service stays inside Kubernetes. Port forwarding creates a temporary route from local port 8080 to Service port 80.

Before you start the route, consider whether your browser connects to a fixed Pod or the stable Service target.

  • Connect local port 8080 to the Service by running this command:
kubectl port-forward service/servicepulse 8080:80

What Does This Command Do?

The command forwards browser traffic to the Service. The Service routes each request to one of its matching Pods.

The forwarding session keeps control of this Ubuntu tab. Stopping the session removes the browser route.

  • Leave this Ubuntu tab running.
  • Return to the browser tab from the standalone-container test.
  • Refresh http://localhost:8080.

You will see the ServicePulse dashboard displaying Version 1 again. This time, the browser reaches a stable Service backed by two managed Pods.

Still Seeing the Outage?

  • Confirm that the port-forwarding command is still running in Ubuntu.
  • Confirm that the Pod list showed two running ServicePulse Pods.
  • Stop any other process using local port 8080.

Help me diagnose why the ServicePulse dashboard is not loading through port forwarding.

Your Kubernetes Checkpoint

  • The k8s/deployment.yaml manifest declares two servicepulse:v1 Pods with a rolling update strategy.
  • The k8s/service.yaml manifest exposes those Pods through a ClusterIP Service.
  • The Deployment has completed its rollout.
  • The Version 1 dashboard is reachable through the Service on local port 8080.

ServicePulse is back online behind a Kubernetes Service. Next, you will scale the Deployment and watch Kubernetes replace a deleted Pod.

Prove Scaling and Self-Healing

ServicePulse v1 now runs across two Pods behind a stable Service. Your next proof is showing that Kubernetes actively manages the workload.

You will scale the Deployment to three replicas. You will delete one Pod to watch the controller restore the declared count.

In this step, get ready to:
  • Scale the live Deployment to three Pods.
  • Align the version-controlled manifest with the live replica count.
  • Prove self-healing by deleting a managed Pod.
Scale the live Deployment

Scaling changes the Deployment's desired replica count. The controller creates enough Pods to reach that number.

  • Scale the live Deployment to three replicas by running this command:
kubectl scale --replicas=3 deployment/servicepulse

What does this command do?

  • The kubectl scale subcommand updates the live Deployment's requested replica count.
  • The --replicas=3 flag declares that three Pods should exist.
  • The Deployment controller creates one additional Pod to close the gap.

You will see a short confirmation that the Deployment was scaled.

Before you check, how many ServicePulse Pod rows do you expect to see?

  • Check the live replica count by running this command:
kubectl get pods -l app=servicepulse

What does this command show?

  • The kubectl command lists Pods whose label matches app=servicepulse.
  • The resulting count shows whether the actual state matches the requested replica count.

You should see three ServicePulse Pod rows. Each Pod reaches the Running state.

That is your first orchestration proof. The Deployment now maintains three running copies of ServicePulse.

Still seeing two Pods?

Confirm that Ubuntu still uses the docker-desktop context. A different context sends the scale request to another cluster.

Wait a few moments if the third Pod is still starting. Help me diagnose why my ServicePulse Deployment is not reaching three Pods.

The live cluster now has three replicas. The version-controlled declaration still says two.

  • Return to Visual Studio Code from earlier.
  • Select k8s/deployment.yaml in the Explorer sidebar.
  • Replace the file contents with this updated manifest:
apiVersion: apps/v1
kind: Deployment
metadata:
  name: servicepulse
  labels:
    app: servicepulse
spec:
  replicas: 3
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxUnavailable: 1
      maxSurge: 1
  selector:
    matchLabels:
      app: servicepulse
  template:
    metadata:
      labels:
        app: servicepulse
    spec:
      containers:
        - name: servicepulse
          image: servicepulse:v1
          imagePullPolicy: IfNotPresent
          ports:
            - name: http
              containerPort: 80

What changed in the manifest?

  • The replicas value now declares three Pods in the project file.
  • The image remains servicepulse:v1 because this step tests orchestration without changing the release.
  • The Deployment labels remain aligned with the Service selector.
  • Save k8s/deployment.yaml.
  • Confirm that k8s/deployment.yaml shows replicas: 3 inside the spec block.

Does the manifest still show two replicas?

Confirm that you edited k8s/deployment.yaml inside the servicepulse workspace. Check that the file tab no longer shows an unsaved-change indicator.

Help me align my ServicePulse manifest with the live replica count.

✔️ Awesome, I've got everything!

Your live Deployment now has three replicas. Your saved manifest declares the same count.

ⓧ I'd like to double check the full code

apiVersion: apps/v1
kind: Deployment
metadata:
  name: servicepulse
  labels:
    app: servicepulse
spec:
  replicas: 3
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxUnavailable: 1
      maxSurge: 1
  selector:
    matchLabels:
      app: servicepulse
  template:
    metadata:
      labels:
        app: servicepulse
    spec:
      containers:
        - name: servicepulse
          image: servicepulse:v1
          imagePullPolicy: IfNotPresent
          ports:
            - name: http
              containerPort: 80

What should match?

Your file should match this manifest exactly. The key change for this step is replicas: 3.

Test self-healing with a Pod deletion

Self-healing becomes visible when actual state falls below desired state. The ReplicaSet responds by creating a replacement Pod.

  • Open a new terminal tab in Ubuntu from earlier.
  • Start watching ServicePulse Pods by running this command:
kubectl get pods -l app=servicepulse -w

What does the watch do?

  • The -l app=servicepulse selector limits the output to this project's Pods.
  • The -w flag keeps the list active so you can observe state changes.
  • The terminal remains occupied until you stop the watch.

Your watch shows the current three Pods. Generated Pod names are long, so copying the complete value can feel fiddly.

  • Open a second terminal tab in Ubuntu from earlier.
  • Record one complete Pod name from the first column here: your generated Pod name.

Before you delete it, do you expect the Deployment to remain at two Pods or restore the count to three?

  • Delete the chosen Pod by replacing <POD-NAME> with your generated Pod name before you run this command:
kubectl delete pod <POD-NAME>

Why does a replacement appear?

  • Deleting the Pod reduces the actual replica count to two.
  • The ReplicaSet detects the missing replica.
  • The controller creates a Pod with a new generated name to restore the declared count.

The watch shows the deleted Pod leaving the list. A replacement with a new name appears before reaching Running.

This recovery is the intended result. Kubernetes restores availability because a controller keeps enforcing the declared replica count.

No replacement Pod appearing?

Confirm that the deleted name came from the list filtered by app=servicepulse. The Deployment only replaces Pods managed through its ReplicaSet.

Help me diagnose why Kubernetes did not replace my deleted ServicePulse Pod.

Capture the restored state

A clean final list turns the recovery into evidence you can show later. It proves that the Deployment returned to three running replicas after a failure.

  • Wait until the watch shows three Pods in the Running state.
  • Stop the watch by pressing Ctrl+C in the watch tab.

Before the final check, how many running Pod rows should the Deployment report?

  • Confirm the restored replica count by running this command:
kubectl get pods -l app=servicepulse

What does the final check prove?

  • Three Pod rows prove that the controller restored the declared replica count.
  • The Running status proves that each replacement completed startup.

You should see three ServicePulse Pod rows. Each row shows the Running state.

Still seeing fewer than three Pods?

Wait for the replacement to finish starting before checking again. Confirm that k8s/deployment.yaml still declares replicas: 3.

Help me find why my ServicePulse Deployment has fewer than three running Pods.

  • Capture a terminal screenshot that shows all three ServicePulse Pods in the Running state.

You have proven that ServicePulse can scale before recovering from a deleted Pod. Next up, you will roll the dashboard to Version 2 before publishing the evidence.

Roll Out v2 and Publish the Evidence

Three ServicePulse Pods now prove that Kubernetes can restore its declared state after a failure. A recruiter still needs evidence that the workload can change safely.

This step releases a new Docker image through a rolling update. You will document the full demonstration in a public GitHub repository.

In this step, get ready to:
  • Build the Version 2 image and roll it out to three Pods.
  • Document the architecture and demonstration in README.md.
  • Publish the finished project to a public GitHub repository.
Release Version 2

A rolling update begins when the Pod template inside a Deployment changes. The new image tag gives Kubernetes a clear release to deploy.

  • Return to site/index.html in the VS Code tab from earlier.
  • Find the version badge that currently displays Version 1.
  • Replace that line with the updated badge below:
<span class="version">Version 2</span>

What does this change do?

The existing version class keeps the badge styling. The new text makes the running release visible in the browser.

  • Save site/index.html.
  • Search the file for Version 1. You should see no remaining match.
  • Find the opening section element with the panel class.
  • Add the release label directly below that opening element by copying this line:
<p class="release">Rolling update complete</p>

Why add a release label?

The label provides visible proof that the browser is serving the updated image. Its existing release class gives it the same green treatment as the version badge.

  • Save site/index.html.
  • Confirm the release label sits immediately above the Deployment story heading.

Does the HTML placement look wrong?

Check that the release paragraph sits inside the existing panel section. Keep it above the second-level heading.

Ask for help checking the edit: Help me place the ServicePulse release label inside the existing panel.

  • Build the Version 2 image from ~/servicepulse by running this command:
docker build -t servicepulse:v2 .

What does this command do?

The build packages the updated site with the existing Dockerfile. The servicepulse:v2 tag preserves Version 1 as a separate local image.

The terminal completes the build and returns to the prompt. Your new image is now ready for the cluster.

Did the image build fail?

Confirm that the terminal is still inside ~/servicepulse. Check that Dockerfile remains at that location.

Ask for targeted help: Why did my ServicePulse Version 2 image fail to build?

  • Return to k8s/deployment.yaml in VS Code.
  • Find the container image line that still references servicepulse:v1.
  • Replace that line with the Version 2 image reference below:
          image: servicepulse:v2

Why change only the image?

The Deployment keeps the same replica count and networking labels. Updating the Pod template image triggers a new rollout while preserving the rest of the declared state.

  • Save k8s/deployment.yaml.
  • Confirm that the manifest still declares 3 replicas.
  • Confirm that the image line now declares servicepulse:v2.

✔️ Awesome, I've got everything!

Your dashboard source now identifies Version 2. Your Deployment now points to the matching image tag.

ⓧ I'd like to double check the full code

Compare your updated dashboard file with this reference.

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>ServicePulse</title>
  <link rel="stylesheet" href="styles.css">
</head>
<body>
  <main class="container">
    <header class="hero">
      <p class="eyebrow">Containerized operations demo</p>
      <div class="title-row">
        <h1>ServicePulse</h1>
        <span class="version">Version 2</span>
      </div>
      <p class="subtitle">A static status dashboard packaged with Docker and orchestrated by Kubernetes.</p>
    </header>

    <section class="status-grid" aria-label="Service status">
      <article class="status-card">
        <span class="indicator" aria-hidden="true"></span>
        <div>
          <h2>API Gateway</h2>
          <p>Operational</p>
        </div>
      </article>
      <article class="status-card">
        <span class="indicator" aria-hidden="true"></span>
        <div>
          <h2>Orders Service</h2>
          <p>Operational</p>
        </div>
      </article>
      <article class="status-card">
        <span class="indicator" aria-hidden="true"></span>
        <div>
          <h2>Notifications</h2>
          <p>Operational</p>
        </div>
      </article>
    </section>

    <section class="panel">
      <p class="release">Rolling update complete</p>
      <h2>Deployment story</h2>
      <ul>
        <li>Packaged as versioned Docker images</li>
        <li>Exposed through a stable Kubernetes Service</li>
        <li>Scaled and restored through declared desired state</li>
      </ul>
    </section>

    <footer>Served by nginx inside Kubernetes</footer>
  </main>
</body>
</html>

What should match?

The version badge reads Version 2. The release paragraph appears inside the existing panel.

Compare your updated Deployment with this reference.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: servicepulse
  labels:
    app: servicepulse
spec:
  replicas: 3
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxUnavailable: 1
      maxSurge: 1
  selector:
    matchLabels:
      app: servicepulse
  template:
    metadata:
      labels:
        app: servicepulse
    spec:
      containers:
        - name: servicepulse
          image: servicepulse:v2
          imagePullPolicy: IfNotPresent
          ports:
            - name: http
              containerPort: 80

What should stay unchanged?

The Deployment still declares three replicas. Its labels and rolling update settings remain intact.

Only the image tag has changed from Version 1 to Version 2.

  • Apply the updated Deployment through standard input by running this command:
cat k8s/deployment.yaml | kubectl apply -f -

What does this command do?

The first command reads the manifest from the Linux workspace. The pipe passes that YAML to the existing kubectl alias.

Kubernetes compares the updated Pod template with the live Deployment. That difference starts the rollout.

Before you check the rollout, do you expect Kubernetes to replace all three Pods at once?

  • Watch the rollout finish and list the resulting Pods by running:
kubectl rollout status deployment/servicepulse
kubectl get pods -l app=servicepulse

What happened during the rollout?

Changing the Pod template created a new ReplicaSet. The Deployment replaced the old Pods at the rate set by maxUnavailable and maxSurge.

The rollout command completes successfully. The Pod list shows three running Pods from the updated release.

Is the rollout stuck?

Check that the manifest references the local servicepulse:v2 image. Confirm that imagePullPolicy: IfNotPresent remains unchanged.

Ask for rollout help: Why is my ServicePulse Version 2 rollout not completing?

  • Return to the Ubuntu tab that ran the earlier port-forward.
  • Press Ctrl+C if that port-forward is still active.

Before you restart the connection, what release do you expect the browser to show after a refresh?

  • Restart access to the ClusterIP Service by running:
kubectl port-forward service/servicepulse 8080:80

Why restart the port-forward?

Port forwarding connects localhost to one Pod selected through the Service. Restarting it creates a fresh session after the rollout.

  • Return to http://localhost:8080 in your browser.
  • Refresh the dashboard.

You should see the Version 2 badge. You should also see Rolling update complete above the deployment story.

That is the release proven. The same Service now reaches the updated workload without changing its stable address.

Still seeing Version 1?

Confirm that you rebuilt the image with the Version 2 tag. Check that the applied Deployment references the same tag.

Ask for help tracing the release: Why does my ServicePulse dashboard still show Version 1?

Document the Demo

A working cluster is difficult to assess without its story. The README turns your commands and observations into reproducible evidence.

  • Return to the ~/servicepulse file tree in VS Code.
  • Use the new-file control to create README.md inside ~/servicepulse.
  • Copy the project introduction and architecture into the new file:
# ServicePulse

ServicePulse is a static operations dashboard that demonstrates the difference between packaging an application with Docker and maintaining its desired state with Kubernetes.

## What this project proves

- I can create a focused Dockerfile and build versioned images.
- I can declare a Kubernetes Deployment and Service in YAML.
- I can expose a cluster-internal Service with port forwarding.
- I can scale a workload, test self-healing, and perform a rolling update.
- I can document failures and explain the reason behind each tool and setting.

## Architecture

```text
Browser
  |
  | kubectl port-forward
  v
ClusterIP Service
  |
  v
Deployment -> ReplicaSet -> nginx Pods
```

What does this section explain?

The opening names the distinction demonstrated by the project. The diagram traces browser traffic through the Service to the managed Pods.

  • Save README.md.
  • Use the Markdown preview control in the editor toolbar. You should see the title and architecture rendered as separate sections.

Does the diagram render incorrectly?

Confirm that both fence lines use three backticks. Keep text directly after the opening fence.

Ask for formatting help: Why is my README architecture diagram not rendering as a code block?

  • Return to the editor view of README.md.
  • Add the repository structure below the architecture section:
## Repository structure

```text
servicepulse/
|-- Dockerfile
|-- .dockerignore
|-- README.md
|-- site/
|   |-- index.html
|   `-- styles.css
`-- k8s/
    |-- deployment.yaml
    `-- service.yaml
```

Why include the file tree?

The tree lets another engineer locate the container definition and Kubernetes manifests quickly. It also shows that the repository contains every artifact needed for the demonstration.

  • Save README.md.
  • Return to the rendered preview. You should see the complete servicepulse file tree below the architecture.

Is the file tree misaligned?

Keep the tree inside its fenced text block. Preserve the spaces before the two Kubernetes manifest entries.

Ask for help checking the tree: Help me fix the alignment of my README repository structure.

  • Return to the editor view of README.md.
  • Add the reproducible build and run instructions below the repository structure:
## Run the demo

Build the current image:

```bash
docker build -t servicepulse:v2 .
```

Apply the Kubernetes resources from Ubuntu while using Docker Desktop's `kubectl.exe` through the `kubectl` alias:

```bash
cat k8s/deployment.yaml | kubectl apply -f -
cat k8s/service.yaml | kubectl apply -f -
kubectl rollout status deployment/servicepulse
```

Open the dashboard:

```bash
kubectl port-forward service/servicepulse 8080:80
```

Visit `http://localhost:8080`.

What makes this reproducible?

The section records the exact commands needed to rebuild the image and apply both manifests. It also records the local URL that displays the result.

  • Save README.md.
  • Return to the rendered preview. You should see three separate command blocks under the demo instructions.

Are the commands running together?

Check that every command group has its own opening fence and closing fence. Keep one blank line around each fenced block.

Ask for Markdown help: Why are my README command blocks merging together?

  • Return to the editor view of README.md.
  • Add the image policy and self-healing explanation below the demo instructions:
## Why versioned tags and IfNotPresent

The project uses `servicepulse:v1` and `servicepulse:v2` instead of `latest`, so the release shown in the manifest is explicit and easy to discuss or roll back. `imagePullPolicy: IfNotPresent` tells Kubernetes to use the local image when it exists and pull only when it is absent, which fits this local Docker Desktop learning cluster.

## Demonstrate scaling and self-healing

```bash
kubectl scale --replicas=3 deployment/servicepulse
kubectl get pods -l app=servicepulse
kubectl delete pod <POD-NAME>
kubectl get pods -l app=servicepulse -w
```

The Deployment notices that the actual Pod count is below the declared count and creates a replacement.

Why preserve these decisions?

This section explains why the local cluster uses versioned image tags and the existing pull policy. The self-healing commands preserve the evidence from your deleted-Pod demonstration.

  • Save README.md.
  • Return to the rendered preview. You should see the scaling commands followed by the replacement explanation.

Is the Pod placeholder missing?

Keep <POD-NAME> in the demonstration command. It tells the reader to substitute one generated Pod name.

Ask for a documentation check: Help me verify the self-healing section of my ServicePulse README.

  • Return to the editor view of README.md.
  • Add the learning and failure record below the self-healing section:
## What I learned / what broke

### What I learned

- Docker packages the files and runtime into an image. A container is a running instance of that image.
- A Kubernetes Deployment declares the desired Pod state and manages ReplicaSets.
- A Service gives changing Pods one stable network target.
- Scaling changes the desired replica count. Self-healing restores it after a failure.
- Changing the Deployment Pod template from v1 to v2 triggers a rolling update.

### What broke

- Stopping the standalone Docker container made the dashboard unavailable.
- Record any setup or rollout error encountered here.

### Root cause and fix

- The standalone container had no controller maintaining desired state.
- Kubernetes fixed that gap by managing replicas through a Deployment.
- Record the root cause and fix for any additional error here.

### Next improvement

Add HTTP health probes and container CPU and memory requests and limits.

Why document the failure?

The outage records the limitation you observed before introducing orchestration. The root-cause section shows that you can connect a visible failure to the controller that resolved it.

  • Save README.md.
  • Return to the rendered preview. You should see separate learning and failure sections with a next improvement.

Are the learning sections hard to scan?

Confirm that each subsection starts with three hash characters. Keep each lesson and failure as a separate bullet.

Ask for help reviewing the structure: Help me check the headings in my learning and failure record.

  • Return to the editor view of README.md.
  • Add the troubleshooting and cleanup reference at the end of the file:
## Troubleshooting

- If `docker` is unavailable in Ubuntu, enable Docker Desktop WSL integration for Ubuntu.
- If kubectl cannot connect, confirm the Docker Desktop cluster is running, select the `docker-desktop` context, and rerun `kubectl cluster-info`.
- If a Pod reports `ImagePullBackOff`, verify that the manifest uses the same versioned image tag that was built locally and that the cluster uses the Kubeadm provisioner.
- If port 8080 is busy, use `kubectl port-forward service/servicepulse 8081:80` and visit `http://localhost:8081`.

## Cleanup

```bash
kubectl delete deployment servicepulse
kubectl delete service servicepulse
docker rm servicepulse-local
docker image rm servicepulse:v1 servicepulse:v2
```

What does this final section preserve?

The troubleshooting list records the project-specific failure paths. The cleanup block gives the reader a direct way to remove the local runtime resources.

  • Save README.md.
  • Return to the rendered preview. You should see the troubleshooting list followed by one cleanup command block.

Is the cleanup block missing?

Confirm that the cleanup heading sits outside the fenced block. Keep all four cleanup commands between the two fence lines.

Ask for a final formatting check: Help me check the end of my ServicePulse README.

✔️ Awesome, I've got everything!

Your README now documents the architecture and exact demo path. It also records the failure that made orchestration necessary.

ⓧ I'd like to double check the full code

# ServicePulse

ServicePulse is a static operations dashboard that demonstrates the difference between packaging an application with Docker and maintaining its desired state with Kubernetes.

## What this project proves

- I can create a focused Dockerfile and build versioned images.
- I can declare a Kubernetes Deployment and Service in YAML.
- I can expose a cluster-internal Service with port forwarding.
- I can scale a workload, test self-healing, and perform a rolling update.
- I can document failures and explain the reason behind each tool and setting.

## Architecture

```text
Browser
  |
  | kubectl port-forward
  v
ClusterIP Service
  |
  v
Deployment -> ReplicaSet -> nginx Pods
```

## Repository structure

```text
servicepulse/
|-- Dockerfile
|-- .dockerignore
|-- README.md
|-- site/
|   |-- index.html
|   `-- styles.css
`-- k8s/
    |-- deployment.yaml
    `-- service.yaml
```

## Run the demo

Build the current image:

```bash
docker build -t servicepulse:v2 .
```

Apply the Kubernetes resources from Ubuntu while using Docker Desktop's `kubectl.exe` through the `kubectl` alias:

```bash
cat k8s/deployment.yaml | kubectl apply -f -
cat k8s/service.yaml | kubectl apply -f -
kubectl rollout status deployment/servicepulse
```

Open the dashboard:

```bash
kubectl port-forward service/servicepulse 8080:80
```

Visit `http://localhost:8080`.

## Why versioned tags and IfNotPresent

The project uses `servicepulse:v1` and `servicepulse:v2` instead of `latest`, so the release shown in the manifest is explicit and easy to discuss or roll back. `imagePullPolicy: IfNotPresent` tells Kubernetes to use the local image when it exists and pull only when it is absent, which fits this local Docker Desktop learning cluster.

## Demonstrate scaling and self-healing

```bash
kubectl scale --replicas=3 deployment/servicepulse
kubectl get pods -l app=servicepulse
kubectl delete pod <POD-NAME>
kubectl get pods -l app=servicepulse -w
```

The Deployment notices that the actual Pod count is below the declared count and creates a replacement.

## What I learned / what broke

### What I learned

- Docker packages the files and runtime into an image. A container is a running instance of that image.
- A Kubernetes Deployment declares the desired Pod state and manages ReplicaSets.
- A Service gives changing Pods one stable network target.
- Scaling changes the desired replica count. Self-healing restores it after a failure.
- Changing the Deployment Pod template from v1 to v2 triggers a rolling update.

### What broke

- Stopping the standalone Docker container made the dashboard unavailable.
- Record any setup or rollout error encountered here.

### Root cause and fix

- The standalone container had no controller maintaining desired state.
- Kubernetes fixed that gap by managing replicas through a Deployment.
- Record the root cause and fix for any additional error here.

### Next improvement

Add HTTP health probes and container CPU and memory requests and limits.

## Troubleshooting

- If `docker` is unavailable in Ubuntu, enable Docker Desktop WSL integration for Ubuntu.
- If kubectl cannot connect, confirm the Docker Desktop cluster is running, select the `docker-desktop` context, and rerun `kubectl cluster-info`.
- If a Pod reports `ImagePullBackOff`, verify that the manifest uses the same versioned image tag that was built locally and that the cluster uses the Kubeadm provisioner.
- If port 8080 is busy, use `kubectl port-forward service/servicepulse 8081:80` and visit `http://localhost:8081`.

## Cleanup

```bash
kubectl delete deployment servicepulse
kubectl delete service servicepulse
docker rm servicepulse-local
docker image rm servicepulse:v1 servicepulse:v2
```

What should the finished file contain?

The finished README contains the architecture and repository tree. It also contains the full demo path and an honest failure record.

The last sections preserve troubleshooting guidance and cleanup commands.

Publish to GitHub

A local folder proves the project only on your machine. Git records the finished state so GitHub can present the same files to someone else.

  • Return to Ubuntu in the existing ~/servicepulse workspace.
  • Initialize the main branch and commit every project file by running:
git init -b main
git add .
git commit -m "Build ServicePulse Docker and Kubernetes demo"

What do these commands record?

The first command starts the repository on the main branch. The next command stages the complete workspace.

The commit stores one named snapshot of the finished project. That snapshot is what you publish.

The terminal should confirm the new commit. Its summary lists the files added to the repository.

Did Git request an author identity?

Follow the exact name and email configuration instructions printed by Git. Rerun the commit command after saving that identity.

Ask for help reading the message: Help me respond to Git's author identity instructions.

  • Use GitHub's top-right creation menu to start a new repository.
  • Enter servicepulse as the repository name.
  • Select public visibility.
  • Leave the starter-file options disabled.
  • Create the empty repository.

The new repository page provides the destination for your local commit. Keeping it empty prevents a conflicting first commit.

  • Replace <YOUR-GITHUB-USERNAME> with your GitHub username in the command below.
  • Connect the local repository to the public repository by running the edited command:
git remote add origin https://github.com/<YOUR-GITHUB-USERNAME>/servicepulse.git

What does this command do?

The remote named origin stores the GitHub repository URL. Future pushes use that name as their destination.

  • Verify the saved remote by running:
git remote -v

What should this verify?

The output should list the ServicePulse repository URL for fetch and push operations. Both entries should use your GitHub username.

Does the remote use the placeholder?

Remove the placeholder from the repository URL before continuing. The URL must use the username shown in your GitHub profile.

Ask for help checking the URL: Help me verify my ServicePulse Git remote without exposing credentials.

GitHub may ask you to authenticate during the push. Complete the sign-in flow without placing any credential inside the project files.

Before you push, what project files do you expect to see on the GitHub repository page?

  • Publish the main branch by running:
git push -u origin main

What does the push publish?

The push sends your committed main branch to GitHub. The upstream setting connects later pushes to the same remote branch.

  • Return to the public servicepulse repository in your browser.
  • Refresh the repository page.

You should see Dockerfile and README.md at the top level. You should also see the site and k8s folders.

The repository page renders the README below the file list. Your architecture and demo commands are now visible to a recruiter.

Did the push fail?

Confirm that the remote URL points to the empty public repository you created. Complete any GitHub authentication request before retrying.

Ask for safe publishing help: Why is my ServicePulse push to GitHub failing?

You made the full demonstration reproducible. ServicePulse now shows a safe Version 2 rollout and the evidence behind it.

Secret mission

Add Health and Resource Guardrails

Harden ServicePulse with HTTP health checks plus explicit CPU and memory guardrails. You will prove that all three Pods remain ready after Kubernetes rolls out the updated configuration.

Clean Up Your Resources

Clean Up Your Resources

This project uses local CPU, memory, and disk only, with no cloud resources or API charges. Decide whether to keep the local stack available, pause it for later, or delete it entirely.

Resources you used:

  • Your stopped servicepulse-local Docker container.
  • Your local servicepulse:v1 and servicepulse:v2 images.
  • Your Kubernetes Deployment with three running Pods.
  • Your servicepulse ClusterIP Service.
  • Your ~/servicepulse project folder in Ubuntu.

Keep everything running

No action needed. Choose this if you are still actively building or want ServicePulse ready for recruiter demonstrations.

  • Keep Docker Desktop running so the local Kubernetes cluster stays available.
  • Retain the stopped standalone container for the packaging-versus-orchestration demonstration.
  • Retain both versioned images for repeatable demonstrations.
  • Keep ~/servicepulse available in Ubuntu.
  • Use the run instructions in README.md to start a new port-forward before each demonstration.
  • Keep your published GitHub servicepulse repository available as recruiter evidence.

Pause - I'll come back to this later

Shut down the active local processes to free memory while keeping the project files and image data for your return.

  • Return to the Ubuntu tab running the port-forward if one is active.
  • Press Ctrl+C to stop the port-forward.
  • Find the Docker whale icon in the Windows system tray.
  • Right-click the Docker whale icon.
  • Choose the menu option that closes Docker Desktop.
  • Leave ~/servicepulse in Ubuntu for your return.

The cluster and port-forward stop consuming active memory. Your project files and local image data remain on disk.

Delete - I don't want to use this again

Remove all local project resources if you want to start fresh. Your published GitHub repository remains available as project evidence.

  • Return to the Ubuntu terminal from earlier.
  • Return to the Ubuntu tab running the port-forward if one is active.
  • Press Ctrl+C to stop the port-forward.
  • Remove the Kubernetes resources, stopped container, and versioned images by running these commands:
kubectl delete deployment servicepulse
kubectl delete service servicepulse
docker rm servicepulse-local
docker image rm servicepulse:v1 servicepulse:v2

What Gets Removed?

  • Deleting the Deployment also removes the three Pods it manages.
  • Deleting the Service removes the stable cluster target for ServicePulse.
  • Removing the stopped container clears the standalone ServicePulse instance.
  • Removing the images clears both local version tags.

Is a Resource Already Missing?

A command may report that its target is missing if you already removed that resource. Continue with the remaining cleanup commands.

Help me diagnose a failed ServicePulse cleanup command.

Local File Deletion Is Permanent

The next command permanently removes ~/servicepulse from Ubuntu. Save another local copy first if you want one.

Your published GitHub repository stays online because it is a separate copy.

  • Delete the Ubuntu project folder by running this command:
rm -rf ~/servicepulse

What Does This Remove?

  • The command removes the entire ~/servicepulse folder from Ubuntu.
  • The local source files and manifests are removed.
  • The local Git history is removed.
  • The published GitHub repository remains available online.

That completes the local cleanup. Your recruiter-facing GitHub evidence remains available.

Nice Work!

Nice Work!

You did it! ServicePulse is now a recruiter-ready Docker plus Kubernetes demo.

You've learned how to:

  • Built a versioned Docker image that serves the ServicePulse dashboard through nginx. Proved the standalone container availability gap by stopping servicepulse-local.
  • Declared the workload through a Kubernetes Deployment plus a stable ClusterIP Service. Scaled the workload to three Pods. Watched self-healing restore the declared desired state after one Pod was deleted.
  • Rolled ServicePulse from servicepulse:v1 to servicepulse:v2 through a controlled Deployment update. Published a recruiter-ready GitHub repository with reproducible instructions plus an honest failure log.
  • Secret Mission: Hardened the Deployment with HTTP readiness probes plus liveness probes. Set explicit CPU requests. Set explicit CPU limits. Set explicit memory requests. Set explicit memory limits.

Ready to quiz yourself?