Containerize a Node.js App with Kubernetes

Package a Node.js app into Docker and deploy it to a local Kubernetes cluster.

Introduction

30 Second Summary

Every time you use an app on your phone or order something online, code is running on a server somewhere. Companies need that code to behave the same way on every machine, whether it's a developer's laptop or a data center halfway across the world.

In this project, you will build a Node.js REST API, package it into a Docker container, and deploy it to a local Kubernetes cluster. You'll see firsthand how code goes from your editor to a running, self-healing service.

What You'll Build

A Node.js REST API containerized with Docker and deployed to a local Kubernetes cluster using Minikube, demonstrating the full container-to-orchestration workflow.

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

  • A containerized Node.js application built with production-ready Docker best practices.
  • Kubernetes Deployment and Service manifests that run your app on a local cluster.
  • Hands-on experience with self-healing, where Kubernetes automatically restarts failed containers.
  • Secret Mission: Scale your deployment to 3 replicas and watch Kubernetes load-balance requests across pods.

Are there any prerequisites?

Basic familiarity with the command line and JavaScript is helpful. You don't need any prior experience with Docker or Kubernetes.

Before We Start

You're about to take a simple application and package it into a portable container with Docker. Then you'll deploy it to Kubernetes, the industry-standard system for running containers at scale.

This is the exact workflow companies use to ship software to production. Before you can build anything, you need the right tools installed. In this step, you'll set up your entire development environment.

In this step, get ready to:
  • Install VS Code, Node.js, and Docker Desktop.
  • Install Minikube and kubectl.
  • Start a local Kubernetes cluster.
Install VS Code, Node.js, and Docker Desktop

You need a code editor, a JavaScript runtime, and a container engine. VS Code is your editor. Node.js runs the application you'll build. Docker Desktop provides the container engine that packages your app into a portable image.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type VS Code and press Enter to open it.

Don't have VS Code installed?

Download it from https://code.visualstudio.com. Run the installer and follow the setup wizard. Once installed, launch it from your search bar.

Next, check if Node.js is already installed on your machine.

  • Open a terminal inside VS Code by pressing Ctrl+` (the backtick key, usually above Tab).
  • Check your Node.js version by running this command:
node --version

✔️ I see a version number

Node.js is ready. Move on to installing Docker Desktop below.

ⓧ Command not found

Node.js is not installed yet. Install it using the instructions for your operating system.

macOS

  • Install Node.js using Homebrew by running this command:
brew install node
Don't have Homebrew?

Download the macOS installer from https://nodejs.org instead and run it.

Windows

  • Install Node.js using winget by running this command in PowerShell:
winget install -e --id OpenJS.NodeJS
  • Close and reopen your terminal after the install finishes.

Now install Docker Desktop. This provides the container engine you'll use to build and run images.

How do I know Docker Desktop is ready?

On Windows, look for a green whale icon in the system tray (bottom-right corner). On macOS, look for the whale icon in the menu bar (top-right corner). Green means the engine is running.

  • Verify Docker is working by running this command in your VS Code terminal:
docker --version

✔️ I see a version number

Docker is installed and running. You're ready for the next tool.

ⓧ Command not found or error

Make sure Docker Desktop is fully launched (check for the green whale icon). If you just installed it, close and reopen your VS Code terminal so the docker command is recognized.

Docker Desktop won't start?

On Windows, Docker Desktop requires WSL 2. If you see an error about WSL, follow the prompt to install the WSL 2 update from Microsoft.

On macOS, the first launch may take a few minutes due to security permission checks.

help me troubleshoot Docker Desktop not starting

Install Minikube and kubectl

Minikube creates a local Kubernetes cluster on your machine. kubectl is the command-line tool you use to talk to that cluster.

macOS

  • Install both Minikube and kubectl using Homebrew by running these commands:
brew install minikube
brew install kubectl
  • Verify both tools are installed by running:
minikube version
kubectl version --client

Windows

  • Open PowerShell and install both tools using winget by running these commands:
winget install Kubernetes.minikube
winget install -e --id Kubernetes.kubectl
  • Close and reopen your terminal, then verify both tools are installed by running:
minikube version
kubectl version --client

✔️ I see version numbers for both

Both tools are installed. You're ready to start your cluster.

ⓧ One or both commands failed

Make sure you closed and reopened your terminal after installing. New commands won't be recognized in a terminal session that was open during the install.

help me fix minikube or kubectl not being recognized

What is Minikube?

Minikube runs a single-node Kubernetes cluster inside a container on your laptop. It gives you a real Kubernetes environment without needing cloud infrastructure.

kubectl is the CLI that sends commands to any Kubernetes cluster. You'll use it to deploy apps, check pod status, and inspect logs.

Start your Kubernetes cluster

With Docker Desktop running in the background, you can now boot up a local Kubernetes cluster using Minikube's Docker driver.

  • Start the cluster by running this command:
minikube start --driver=docker

What does this command do?

The --driver=docker flag tells Minikube to use Docker Desktop's container engine to run the Kubernetes node. This avoids needing a separate virtual machine or hypervisor.

The first time you run this, Minikube downloads container images needed for Kubernetes. This may take several minutes depending on your internet speed.

  • Once the command finishes, verify the cluster is ready by running:
kubectl get nodes

✔️ I see one node with status Ready

Your Kubernetes cluster is running. You have a single node ready to accept deployments.

ⓧ I see an error or no nodes

Make sure Docker Desktop is fully running (green whale icon). If you see a connection error, wait a moment and try kubectl get nodes again. The cluster may still be initializing.

help me troubleshoot minikube start failing

Minikube start takes forever or fails?

Docker Desktop must be fully initialized before Minikube can start. Wait for the green whale icon in your system tray or menu bar.

If you see a timeout error, your internet connection may be slow. The first start downloads several container images. Try running minikube start --driver=docker again after it fails.

help me fix minikube start failing with Docker driver

Your development environment is ready. Next up, you'll build the Node.js application that gets containerized and deployed to your new cluster.

Install Docker Desktop and Minikube

Before you start building, you need to confirm that your tools are set up correctly. Docker Desktop, Minikube, and kubectl work together to form your local Kubernetes development environment.

Docker Desktop provides the container runtime that Minikube uses to run your cluster. In this step, you'll verify each tool is responding correctly so you can confidently move into building your app.

In this step, get ready to:
  • Verify Docker Desktop is running.
  • Verify Minikube and kubectl are installed.
  • Confirm your Kubernetes cluster is ready.
Verify Docker Desktop is running

Docker Desktop powers the container engine that Minikube relies on. Let's confirm it's responding to commands.

  • Open a terminal in VS Code by pressing Ctrl+` (macOS) or Ctrl+` (Windows).
  • Check that Docker is running by running this command:
docker --version

You should see output showing a version number, like Docker version 28.x.x, build abc123.

Don't see a version number?

Check that Docker Desktop is running by looking for the whale icon in your system tray. If it's not there, search for Docker Desktop in your system search and launch it.

Wait until the whale icon turns green before retrying the command. help me troubleshoot my Docker Desktop not responding to commands

Verify Minikube and kubectl are installed

Minikube creates a local Kubernetes cluster on your machine. kubectl is the command-line tool you use to interact with that cluster.

  • Check your Minikube installation by running this command:
minikube version

You should see a version string printed in your terminal.

  • Check your kubectl installation by running this command:
kubectl version --client

You should see client version information displayed.

What do these tools do?

  • minikube version confirms that the Minikube binary is installed and accessible from your terminal.
  • kubectl version --client confirms that kubectl can run independently of a cluster connection. The --client flag skips checking the server, so it works even if no cluster is running yet.

Command not found?

If your terminal does not recognize minikube or kubectl, close your terminal and reopen it. Newly installed tools sometimes require a fresh terminal session to appear in your PATH.

help me fix minikube or kubectl command not found in my terminal

Confirm your Kubernetes cluster is running

Your Minikube cluster is already running with the Docker driver. Let's confirm it's healthy.

  • Check the cluster node status by running this command:
kubectl get nodes

You should see one node with status Ready. This confirms your local Kubernetes cluster is healthy and ready to accept deployments.

What is a node?

A node is a machine (physical or virtual) that runs your containerized applications in Kubernetes. Minikube gives you a single-node cluster, which is all you need for local development and learning.

Node not showing Ready?

Make sure Docker Desktop is fully started (green whale icon). Minikube uses Docker as its container runtime, so the Docker engine must be running first.

If the node shows a status other than Ready, wait 30 seconds and run kubectl get nodes again. The cluster may still be initializing.

help me troubleshoot my Minikube cluster not showing Ready status

Your environment is confirmed and ready. Next up, you'll build the Node.js application that will eventually run inside your Kubernetes cluster.

Build the Node.js Application

Your development environment is ready. Docker Desktop, Minikube, and kubectl are all installed, and your Kubernetes cluster is running with one node.

Now you need an application to containerize and deploy. In this step, you'll build a simple Node.js REST API that returns system information like the hostname and uptime. This makes it easy to verify which pod is handling each request once you deploy to Kubernetes.

In this step, get ready to:
  • Create a project folder and install dependencies.
  • Write a Node.js Express API that returns system info.
  • Test the app locally before containerizing.
Create the project structure

Every Node.js project starts with a folder, a package.json file that declares its dependencies, and an npm install to download them.

  • In VS Code, open the integrated terminal by pressing Ctrl+` (Windows) or Cmd+` (macOS).
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir k8s-demo-app
cd k8s-demo-app
  • Confirm the folder exists by running:
ls

You should see the k8s-demo-app folder listed in the output.

  • In VS Code, click File in the top menu bar.
  • Click Open Folder.
  • Select the k8s-demo-app folder on your Desktop, then click Select Folder.
  • If prompted to trust the authors, click Yes, I trust the authors.
  • Open a new terminal in this folder by pressing Ctrl+` (Windows) or Cmd+` (macOS).

Your terminal prompt now shows k8s-demo-app as the current directory.

  • Create a new file called package.json in VS Code by clicking File then New File.
  • Paste the following content into package.json:
{
  "name": "k8s-demo-app",
  "version": "1.0.0",
  "description": "A simple Node.js app for Kubernetes deployment",
  "main": "server.js",
  "scripts": {
    "start": "node server.js"
  },
  "dependencies": {
    "express": "4.21.2"
  }
}

What does this file do?

The package.json file is like an identity card for your Node.js project. It declares the project name, the entry point (server.js), and which packages your app needs.

The dependencies section tells npm to install Express version 4.21.2, the web framework your API will use.

  • Save package.json by pressing Ctrl+S (Windows) or Cmd+S (macOS).
  • Install the project dependencies by running this command in your terminal:
npm install

You should see output showing packages added, and a new node_modules folder appears in the VS Code Explorer sidebar.

Seeing an error during npm install?

  • Make sure you saved package.json before running the command.
  • Check that your terminal is inside the k8s-demo-app folder. Run pwd to confirm your location.
  • Verify Node.js is installed by running node --version in your terminal.

Still stuck? Help me troubleshoot npm install failing in my k8s-demo-app project

✔️ Awesome, I've got everything!

Great. Make sure package.json is saved and you can see the node_modules folder in your sidebar.

ⓧ I'd like to double check the full code

Your package.json should look exactly like this:

{
  "name": "k8s-demo-app",
  "version": "1.0.0",
  "description": "A simple Node.js app for Kubernetes deployment",
  "main": "server.js",
  "scripts": {
    "start": "node server.js"
  },
  "dependencies": {
    "express": "4.21.2"
  }
}
Write the application code

Your app will be a REST API with two routes. The main route returns system information as JSON, and a health check route tells Kubernetes whether the app is running properly.

  • Create a new file called server.js by clicking File then New File in VS Code.
  • Paste the following code into server.js:
// Import Express framework and the built-in OS module
const express = require('express');
const os = require('os');

// Create the Express app and define the port
const app = express();
const PORT = 3000;

// Main route - returns system info as JSON
app.get('/', (req, res) => {
  res.json({
    message: 'Hello from Kubernetes!',
    hostname: os.hostname(),
    timestamp: new Date().toISOString(),
    platform: os.platform(),
    uptime: process.uptime()
  });
});

// Health check route - Kubernetes uses this to monitor the app
app.get('/health', (req, res) => {
  res.status(200).json({ status: 'healthy' });
});

// Start the server on all network interfaces
app.listen(PORT, '0.0.0.0', () => {
  console.log(`Server running on port ${PORT}`);
});

What does this code do?

  • The / route returns JSON with the machine's hostname, current timestamp, platform, and uptime. In Kubernetes, the hostname becomes the pod name, so you can see which pod handled each request.
  • The /health route returns a simple { status: 'healthy' } response. Kubernetes will ping this endpoint to check if your app is alive.
  • The server listens on 0.0.0.0 (all network interfaces) instead of just localhost. This is required for the app to be reachable from outside a container.
  • Save server.js by pressing Ctrl+S (Windows) or Cmd+S (macOS).

✔️ Awesome, I've got everything!

Great. Make sure you saved server.js and you can see it listed in the Explorer sidebar.

ⓧ I'd like to double check the full code

Your server.js should look like this:

const express = require('express');
const os = require('os');

const app = express();
const PORT = 3000;

app.get('/', (req, res) => {
  res.json({
    message: 'Hello from Kubernetes!',
    hostname: os.hostname(),
    timestamp: new Date().toISOString(),
    platform: os.platform(),
    uptime: process.uptime()
  });
});

app.get('/health', (req, res) => {
  res.status(200).json({ status: 'healthy' });
});

app.listen(PORT, '0.0.0.0', () => {
  console.log(`Server running on port ${PORT}`);
});
Test the app locally

Before containerizing your app, verify it works on its own. Running it locally first helps you catch bugs before adding Docker complexity.

  • Start the application by running this command in your terminal:
node server.js

You should see Server running on port 3000 printed in your terminal.

  • Open your browser and visit http://localhost:3000.

What should I see?

Your browser displays a JSON response with five fields: message, hostname, timestamp, platform, and uptime. The hostname shows your computer's name. Once deployed to Kubernetes, this will show the pod name instead.

  • Return to your terminal and press Ctrl+C to stop the server.

Not seeing the JSON response?

  • Make sure the terminal still shows Server running on port 3000. If not, the server crashed on startup. Check for typos in server.js.
  • If you see an error about port 3000 already in use, another process is using that port. Close other terminals or applications that might be using it, then try again.
  • Make sure you visit http://localhost:3000 (not https).

Still stuck? Help me debug why my Node.js Express server isn't responding on localhost:3000

Your application works locally. Next up, you'll package it into a Docker container so it can run anywhere.

Containerize with Docker

Your Node.js app runs perfectly on your machine. The problem is, "works on my machine" does not help when you need to deploy to a server or share with a team.

Docker solves this by packaging your application, its runtime, and all its dependencies into a single portable unit called a container. In this step, you will write a Dockerfile, build a Docker image, and verify everything works inside a container.

In this step, get ready to:
  • Create a Dockerfile with production best practices.
  • Create a .dockerignore file to keep the build context clean.
  • Build and test a Docker container locally.
Create the Dockerfile

A Dockerfile is a text file containing instructions that Docker uses to build an image. Each instruction creates a layer, and Docker caches these layers to speed up future builds.

  • In VS Code, create a new file called Dockerfile (no file extension) inside your k8s-demo-app folder.
  • Add the following code to your Dockerfile:
# Use a lightweight Node.js base image
FROM node:20-alpine

# Set the working directory inside the container
WORKDIR /app

# Copy dependency files first for layer caching
COPY package.json package-lock.json* ./

# Install production dependencies only
RUN npm ci --only=production

# Copy application source code
COPY server.js ./

# Create a non-root user for security
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser

# Document the port the app listens on
EXPOSE 3000

# Start the application
CMD ["node", "server.js"]

What does this Dockerfile do?

This Dockerfile uses a layered strategy for efficiency and security:

  • FROM node:20-alpine starts from a minimal Alpine Linux image with Node.js pre-installed. Alpine images are much smaller than full Ubuntu-based images.
  • COPY package.json is done before copying source code. Docker caches each layer, so if your dependencies have not changed, Docker skips re-installing them on the next build.
  • npm ci --only=production installs exact versions from the lockfile and skips dev dependencies, keeping the image lean.
  • The adduser and USER lines create and switch to a non-root user. Running containers as root is a security risk.
  • Save Dockerfile.

File named Dockerfile.txt by accident?

Windows sometimes adds a hidden .txt extension. In VS Code, check the tab title reads exactly Dockerfile with no extension. If it shows Dockerfile.txt, right-click the file in the Explorer sidebar and select Rename to remove the extension.

help me fix my Dockerfile name

✔️ Awesome, I've got everything!

Great. Double check you have saved your file before moving on.

ⓧ I'd like to double check the full code

FROM node:20-alpine

WORKDIR /app

COPY package.json package-lock.json* ./

RUN npm ci --only=production

COPY server.js ./

RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser

EXPOSE 3000

CMD ["node", "server.js"]
Create a .dockerignore file

A .dockerignore file tells Docker which files to exclude when copying the build context. This keeps your image small and avoids leaking unnecessary files into the container.

  • Create a new file called .dockerignore in your k8s-demo-app folder.
  • Add the following content to .dockerignore:
node_modules
.git
.gitignore
README.md
*.log

Why exclude node_modules?

Your Dockerfile already runs npm ci to install dependencies inside the container. Copying your local node_modules would be redundant and could introduce platform-incompatible binaries. Your Windows modules will not work inside the Linux container.

  • Save .dockerignore.
  • Confirm both new files exist by running:
ls

You should see Dockerfile, .dockerignore, node_modules, package.json, and server.js listed in the output.

✔️ Awesome, I've got everything!

Great. Double check you have saved the file before moving on.

ⓧ I'd like to double check the full code

node_modules
.git
.gitignore
README.md
*.log
Build and test the container

Now you will build a Docker image from your Dockerfile and run it as a container to confirm everything works.

  • Build your Docker image by running this command:
docker build -t k8s-demo-app:1.0 .

What does this command do?

  • docker build reads your Dockerfile and executes each instruction to create an image.
  • -t k8s-demo-app:1.0 tags the image with a name and version so you can reference it later.
  • . tells Docker to use the current directory as the build context (where it looks for your Dockerfile and source files).

The build may take a minute the first time as Docker downloads the node:20-alpine base image. You should see output ending with a successful build confirmation.

Build failed with an error?

  • Make sure Docker Desktop is running. Check for the whale icon in your system tray.
  • Verify your terminal is inside the k8s-demo-app folder where the Dockerfile lives. Run cd to check your current directory.
  • Check that your file is named exactly Dockerfile (capital D, no file extension).

help me debug my Docker build failure

  • Run a container from your new image by running this command:
docker run -d -p 3000:3000 --name demo-test k8s-demo-app:1.0

What does this command do?

  • -d runs the container in detached mode (in the background).
  • -p 3000:3000 maps port 3000 on your machine to port 3000 inside the container.
  • --name demo-test gives the container a friendly name so you can reference it easily.
  • Open http://localhost:3000 in your browser.

You should see the same JSON response as before, with a hostname field showing a random container ID instead of your computer name. This confirms your app is running inside Docker.

Page not loading at localhost:3000?

  • Make sure you stopped the Node.js server from the previous step. If it is still running on port 3000, the Docker container cannot bind to that port.
  • Check the container is running with docker ps. If it is not listed, check the logs with docker logs demo-test to see what went wrong.

help me fix my Docker container

  • Now that you have confirmed it works, stop and remove the test container by running these commands:
docker stop demo-test
docker rm demo-test

This frees up port 3000 and removes the container. The image itself stays on your system for deployment later.

Your app is containerized and proven to work inside Docker. Next, you will load this image into your Minikube cluster so Kubernetes can run it.

Load the Image into Minikube

Your Docker image is built and tested on your local machine. However, Minikube runs its own isolated Docker daemon inside the cluster. It cannot see images stored on your host machine's Docker.

To deploy your app to Kubernetes, you need to explicitly load the image into Minikube's internal container runtime. This bridges the gap between your local build and the cluster.

In this step, get ready to:
  • Load your Docker image into Minikube's container runtime.
  • Verify the image is available inside the cluster.
Make the image available to Minikube

Your host machine and Minikube each have their own Docker daemon. Think of them as two separate computers that don't share files automatically.

  • Load your local Docker image into Minikube by running this command:
minikube image load k8s-demo-app:1.0

What does this command do?

The minikube image load command copies your locally built Docker image from your host's Docker daemon into Minikube's internal Docker daemon.

This means you don't need to push your image to a remote container registry like Docker Hub. It's a shortcut for local development that keeps everything on your machine.

Why not use a container registry?

In production, teams push images to a registry (like Docker Hub or AWS ECR) and Kubernetes pulls from there. For local development, minikube image load skips that complexity entirely. No account setup, no authentication, no network transfer.

This command may take 30-60 seconds depending on the image size. Wait for it to complete before continuing.

  • Verify the image is available inside Minikube by running:
minikube image list | findstr k8s-demo-app

You should see a line containing k8s-demo-app in the output. This confirms Minikube can access your image when you create a Kubernetes deployment.

Don't see your image in the list?

  • Make sure Docker Desktop is still running. Minikube needs access to the Docker daemon to load images.
  • Confirm the image exists locally by running docker images k8s-demo-app. If it's not listed, go back to the previous step and rebuild it.
  • If the load command timed out or showed an error, try running it again. Network hiccups between Docker daemons can cause intermittent failures.

Help me troubleshoot why my image isn't showing up in Minikube

Your image is now inside the cluster and ready for Kubernetes to use. Next up, you'll write the Kubernetes manifests that tell the cluster how to run your containerized app.

Deploy to Kubernetes

Your Docker image is loaded into Minikube and ready to run. Now you need to tell Kubernetes exactly how to run it.

Kubernetes uses manifest files to describe the desired state of your application. You'll create two manifests: one that defines how your container runs, and one that defines how network traffic reaches it.

In this step, get ready to:
  • Create a Deployment manifest to define how your app runs.
  • Create a Service manifest to expose your app to the network.
  • Apply both manifests and verify your app is running on the cluster.
Create the Deployment manifest

A Deployment is a Kubernetes resource that manages your application's lifecycle. It defines which container image to run, how many copies (replicas) to maintain, and how to check if the app is healthy.

  • In VS Code, create a new file called deployment.yaml in your k8s-demo-app folder.
  • Paste the following manifest into the file:
apiVersion: apps/v1
kind: Deployment
metadata:
  name: k8s-demo-app
  labels:
    app: k8s-demo-app
spec:
  replicas: 1
  selector:
    matchLabels:
      app: k8s-demo-app
  template:
    metadata:
      labels:
        app: k8s-demo-app
    spec:
      containers:
        - name: k8s-demo-app
          image: k8s-demo-app:1.0
          imagePullPolicy: Never
          ports:
            - containerPort: 3000
          resources:
            requests:
              memory: "64Mi"
              cpu: "100m"
            limits:
              memory: "128Mi"
              cpu: "250m"
          livenessProbe:
            httpGet:
              path: /health
              port: 3000
            initialDelaySeconds: 5
            periodSeconds: 10
          readinessProbe:
            httpGet:
              path: /health
              port: 3000
            initialDelaySeconds: 3
            periodSeconds: 5

What does this manifest do?

  • replicas: 1 tells Kubernetes to run exactly one copy of your container.
  • image: k8s-demo-app:1.0 points to the Docker image you loaded into Minikube in the previous step.
  • imagePullPolicy: Never prevents Kubernetes from trying to pull the image from Docker Hub. It uses the locally loaded image instead.
  • livenessProbe and readinessProbe hit your /health endpoint to check if the app is alive and ready to receive traffic.
  • resources sets memory and CPU guardrails so one container can't consume all cluster resources.
  • Save deployment.yaml.
  • Confirm the file exists by running:
ls

You should see deployment.yaml listed alongside your other project files.

Don't see deployment.yaml?

Make sure you saved the file in the k8s-demo-app folder, not a subfolder or your Desktop. Check that the filename is exactly deployment.yaml with no extra extension like .txt.

help me find where my deployment.yaml file was saved

✔️ Awesome, I've got everything!

Great. Double check you've saved your file before moving on.

ⓧ I'd like to double check the full code

apiVersion: apps/v1
kind: Deployment
metadata:
  name: k8s-demo-app
  labels:
    app: k8s-demo-app
spec:
  replicas: 1
  selector:
    matchLabels:
      app: k8s-demo-app
  template:
    metadata:
      labels:
        app: k8s-demo-app
    spec:
      containers:
        - name: k8s-demo-app
          image: k8s-demo-app:1.0
          imagePullPolicy: Never
          ports:
            - containerPort: 3000
          resources:
            requests:
              memory: "64Mi"
              cpu: "100m"
            limits:
              memory: "128Mi"
              cpu: "250m"
          livenessProbe:
            httpGet:
              path: /health
              port: 3000
            initialDelaySeconds: 5
            periodSeconds: 10
          readinessProbe:
            httpGet:
              path: /health
              port: 3000
            initialDelaySeconds: 3
            periodSeconds: 5
Create the Service manifest

A Service is the Kubernetes resource that exposes your application to network traffic. Without a Service, your container runs but nothing can reach it from outside the cluster.

  • Create a new file called service.yaml in your k8s-demo-app folder.
  • Paste the following manifest into the file:
apiVersion: v1
kind: Service
metadata:
  name: k8s-demo-service
spec:
  type: NodePort
  selector:
    app: k8s-demo-app
  ports:
    - protocol: TCP
      port: 80
      targetPort: 3000

What does this Service do?

type: NodePort exposes the Service on a port that's accessible from outside the cluster. This is what lets you open the app in your browser.

selector: app: k8s-demo-app tells the Service to route traffic to any Pod with the label app: k8s-demo-app. This matches the labels in your Deployment template.

port: 80 is the port the Service listens on. targetPort: 3000 is the port your container is actually running on. The Service bridges the two.

  • Save service.yaml.
  • Confirm both manifest files exist by running:
ls

You should see both deployment.yaml and service.yaml in the output.

Missing service.yaml?

Double check you created the file in the same k8s-demo-app folder as deployment.yaml. YAML files are whitespace-sensitive, so make sure your indentation uses spaces, not tabs.

help me check why my service.yaml isn't showing up

✔️ Awesome, I've got everything!

Great. Double check you've saved your file before moving on.

ⓧ I'd like to double check the full code

apiVersion: v1
kind: Service
metadata:
  name: k8s-demo-service
spec:
  type: NodePort
  selector:
    app: k8s-demo-app
  ports:
    - protocol: TCP
      port: 80
      targetPort: 3000
Apply the manifests to your cluster

Your manifest files describe the desired state of your application. Now you'll hand them to Kubernetes and let it create the actual resources.

  • Deploy your application by running these commands:
kubectl apply -f deployment.yaml
kubectl apply -f service.yaml

What does kubectl apply do?

kubectl apply reads your manifest file and sends it to the Kubernetes API server. The API server then creates (or updates) the resources described in the file.

You should see deployment.apps/k8s-demo-app created and service/k8s-demo-service created as confirmation output.

  • Verify your deployment is running by checking all three resource types:
kubectl get deployments
kubectl get pods
kubectl get services

In the kubectl get pods output, you should see one pod with a STATUS of Running.

What should I see?

  • kubectl get deployments shows your k8s-demo-app deployment with 1/1 replicas ready.
  • kubectl get pods shows a pod named k8s-demo-app- followed by a random suffix, with STATUS Running and READY 1/1.
  • kubectl get services shows k8s-demo-service with TYPE NodePort and PORT(S) mapping 80 to a dynamically assigned node port.

Pod status showing something other than Running?

If you see ErrImageNeverPull, the image was not loaded into Minikube correctly. Run minikube image load k8s-demo-app:1.0 again, then delete the pod with kubectl delete pod -l app=k8s-demo-app and let Kubernetes recreate it.

If you see CrashLoopBackOff, there's an error in your application code. Run kubectl logs -l app=k8s-demo-app to see the error message.

help me debug why my Kubernetes pod isn't running

Your application is now running on Kubernetes. In the next step, you'll access it in your browser and explore how Kubernetes keeps it alive.

Access Your Application

Your application is deployed and the Kubernetes cluster is running it inside a pod. Now it's time to see it live in your browser.

In this step, you'll use Minikube's built-in service tunnel to access your app from outside the cluster. You'll also inspect the deployment and test one of Kubernetes' most powerful features: self-healing.

In this step, get ready to:
  • Open the service in your browser via Minikube.
  • Inspect the deployment with kubectl commands.
  • Test self-healing by deleting a pod and watching Kubernetes recreate it.
Open the service in your browser

Your app is running inside the cluster, but the cluster's network is isolated from your machine. The NodePort Service you created exposes a port on the cluster's node, and Minikube can tunnel traffic from your machine to that port.

  • Open your application in the browser by running this command:
minikube service k8s-demo-service

What does this command do?

Minikube creates a tunnel from your host machine to the NodePort Service inside the cluster. It then opens your default browser to the tunnelled URL.

Because you're using the Docker driver, the terminal needs to stay open to keep the tunnel alive. You'll see the URL printed in the terminal output.

  • In your browser, you should see a JSON response containing a message, hostname, timestamp, platform, and uptime field.
  • The hostname value is the pod's name. This will be useful later when testing load balancing across multiple pods.

Browser didn't open?

If the browser doesn't open automatically, look for a URL printed in the terminal output (something like http://127.0.0.1:<port>). Copy and paste it into your browser manually.

Make sure Docker Desktop is still running. The tunnel depends on it.

Help me troubleshoot this

Explore your deployment

Now that your app is reachable, you can use kubectl to look under the hood and see how Kubernetes is managing your pod.

  • Open a new terminal window (keep the tunnel terminal running).
  • View detailed information about your deployment by running:
kubectl describe deployment k8s-demo-app

What does this command show?

The describe command shows a summary of your deployment including the number of replicas, the container image, resource limits, health probes, and recent events like pod scheduling.

  • View the application logs from your running pod by running:
kubectl logs -l app=k8s-demo-app

What does this show?

The -l app=k8s-demo-app flag selects pods by their label. You should see the Server running on port 3000 message from your Express app's startup, plus any request logs from the health probes.

Seeing 'no resources found'?

Double check the label matches your deployment. Run kubectl get pods --show-labels to confirm your pod has the app=k8s-demo-app label.

Help me fix this

Test self-healing

One of Kubernetes' most important features is self-healing. If a pod crashes or gets deleted, the Deployment controller automatically creates a replacement to maintain the desired number of replicas.

  • Delete the running pod by running:
kubectl delete pod -l app=k8s-demo-app
  • You should see a confirmation message like pod "k8s-demo-app-xxxxx" deleted.
  • Immediately watch the pods list to observe self-healing in action by running:
kubectl get pods --watch

What should I see?

Within seconds, a new pod appears with a different name. You'll see it transition through states: Pending, then ContainerCreating, then Running.

The Deployment noticed the actual state (0 pods) didn't match the desired state (1 pod) and immediately created a replacement. This is self-healing.

  • Press Ctrl+C to stop watching once the new pod shows Running status.

Pod not coming back?

Make sure your Deployment is still present by running kubectl get deployments. If it shows 0 desired replicas, something deleted the Deployment itself, not just the pod.

Help me troubleshoot this

You've now seen Kubernetes self-healing in action. Your app can survive pod failures without any manual intervention. Next up, the Secret Mission challenges you to scale your deployment and observe load balancing across multiple pods.

Secret mission

Scale and Load-Balance

Your app is running on one pod, but production systems handle thousands of requests per second. Scale your deployment to 3 replicas and watch Kubernetes distribute traffic across all of them automatically.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely on your local machine, so there are no ongoing cloud costs. However, Minikube and Docker Desktop use system resources (RAM and CPU) while running. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.

Resources you used:

  • Minikube cluster (includes the Kubernetes Deployment, Service, and metrics-server addon).
  • Docker image k8s-demo-app:1.0.
  • Project folder k8s-demo-app on your Desktop.
  • Docker Desktop, Minikube, and kubectl (installed tools).

Keep everything running

No action needed. Choose this if you plan to continue experimenting with your Kubernetes cluster or Docker images. Minikube uses approximately 2GB of RAM while running.

  • Docker Desktop stays running in your system tray.
  • Your Minikube cluster continues running with 3 replicas of your application.
  • Minikube and kubectl remain installed and available from any terminal.

Pause - I'll come back to this later

Stop the Minikube cluster to free up RAM and CPU. Your cluster state is preserved and you can restart it later.

  • Stop the Minikube cluster by running:
minikube stop
  • You can optionally quit Docker Desktop from the system tray icon to free additional memory.
  • When you're ready to resume, open Docker Desktop and run minikube start to bring your cluster back.

Delete - I don't want to use this again

Remove all project resources and start fresh. This deletes the cluster, the Docker image, the project files, and the installed tools from your machine.

  • Delete the Minikube cluster (removes all Kubernetes resources, addons, and the cluster VM) by running:
minikube delete
  • Remove the Docker image by running:
docker rmi k8s-demo-app:1.0
  • Delete the project folder. On Windows (PowerShell), run:
cd ~/Desktop
Remove-Item -Recurse -Force k8s-demo-app
  • On macOS or Linux, run:
cd ~/Desktop
rm -rf k8s-demo-app
  • Uninstall Minikube and kubectl by running:
winget uninstall Kubernetes.minikube
winget uninstall -e --id Kubernetes.kubectl
  • Uninstall Docker Desktop through Windows Settings.
  • Open Settings, select Apps, find Docker Desktop, and click Uninstall.

That's a Wrap!

That's a Wrap!

Nice work! You just took a Node.js application from source code to a running, self-healing deployment on a local Kubernetes cluster.

Here's what you learned:

  • Built a production-ready Dockerfile with best practices including an Alpine base image, layer caching, and a non-root user.
  • Created Kubernetes Deployment and Service manifests to define how your application runs, heals itself, and exposes traffic.
  • Used kubectl to deploy, inspect, and test self-healing on a live Minikube cluster.
  • Scaled your deployment to 3 replicas and observed Kubernetes load-balancing requests across pods.

You now understand the core Docker-to-Kubernetes workflow that powers modern cloud-native applications at scale.

Ready to quiz yourself?