Deploy a Node.js App to Kubernetes

Containerize a Node.js app and deploy it to Azure Kubernetes Service.

Introduction

30 Second Summary

Every time you open an app or load a website, there are servers behind the scenes keeping things running. When millions of people use a service at once, companies need a way to run many copies of their app and automatically recover when something breaks.

In this project, you will build a Node.js API, package it into a Docker container, and deploy it to a Kubernetes cluster on Azure. You will watch your app automatically recover from crashes and handle traffic across multiple copies.

What You'll Build

A Node.js Express API that returns system information, containerized with Docker and deployed to both a local Kubernetes cluster and a live Azure Kubernetes Service cluster.

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

  • A containerized Node.js API running on a local Kubernetes cluster via Docker Desktop.
  • A live deployment on Azure Kubernetes Service accessible from the internet with a public IP address.
  • Hands-on proof of Kubernetes self-healing and scaling across multiple pods.
  • Secret Mission: Deploy a new version of your app with zero downtime using Kubernetes rolling updates.

Are there any prerequisites?

You should have basic JavaScript knowledge, Docker Desktop installed, and VS Code installed. No prior Kubernetes or Azure experience is needed.

Install Node.js and Azure CLI

Before you can build and deploy your app, you need the right tools on your machine.

In this step, you will install Node.js (the JavaScript runtime your app runs on), the Azure CLI (a command-line tool for managing Azure cloud resources), and enable Kubernetes inside Docker Desktop so you can test deployments locally.

In this step, get ready to:
  • Install Node.js LTS (v24) and verify it works.
  • Install the Azure CLI and verify it works.
  • Enable Kubernetes in Docker Desktop and verify kubectl.
Install Node.js

Node.js is the runtime that executes your JavaScript code outside a browser. Your Express API depends on it.

  • Open your browser and go to https://nodejs.org/en/download.
  • Download the LTS installer for your operating system (.msi for Windows, .pkg for macOS).
  • Run the installer and accept all the default options.

What is Node.js LTS?

LTS stands for Long Term Support. Node.js v24 (codename Krypton) is the current LTS version, meaning it receives bug fixes and security patches for an extended period. This makes it the safest choice for projects.

Once the installer finishes, you need to verify that Node.js is available in your terminal.

  • Open a terminal. Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Terminal (macOS) or Command Prompt (Windows) and press Enter to open it.
  • Check your Node.js version by running:
node --version

✔️ I see version 24 or higher

You should see something like v24.16.0. Node.js is ready to go.

  • Also confirm npm (the Node.js package manager) is installed by running:
npm --version

You should see a version number printed. Both tools are ready.

ⓧ I see an older version

If you see a version number lower than v24 (for example v20 or v22), you need to upgrade.

  • Go to https://nodejs.org/en/download and download the LTS installer.
  • Run the installer. It will replace your current version with v24.
  • Close and reopen your terminal, then run node --version again to confirm it shows v24 or higher.

ⓧ Command not found

This means Node.js is not installed yet.

  • Go to https://nodejs.org/en/download and download the LTS installer for your operating system.
  • Run the installer and accept all default options.
  • Close and reopen your terminal, then run node --version again.
  • You should now see v24.16.0 or similar.

Still seeing the old version after reinstalling?

Make sure you fully closed and reopened your terminal after installing. Some terminals cache the old path until restarted.

On macOS, if you previously installed Node.js via a version manager like nvm, you may need to run nvm install 24 and nvm use 24 instead.

Help me troubleshoot my Node.js installation.

Install Azure CLI

The Azure CLI lets you create and manage Azure cloud resources directly from your terminal. You will use it to create a container registry and a Kubernetes cluster on Azure.

  • Install the Azure CLI by running the command for your operating system:

Windows

  • Run this command in your terminal:
winget install --exact --id Microsoft.AzureCLI

What does this command do?

WinGet is the built-in Windows package manager. This command tells it to install the Azure CLI package using its exact ID, so there is no ambiguity about which package to install.

macOS

  • Run this command in your terminal:
brew update && brew install azure-cli

What does this command do?

Homebrew is the most popular package manager on macOS. This command updates Homebrew's package index, then installs the Azure CLI.

Seeing 'brew: command not found'?

You need to install Homebrew first. Visit https://brew.sh and follow the one-line install command on the homepage. Then retry the Azure CLI install command above.

  • Close and reopen your terminal so the new command is available.
  • Verify the installation by running:
az version

✔️ I see version information

You should see JSON output including a version number (such as 2.86.0). The Azure CLI is ready.

ⓧ Command not found

This means the install did not register the az command in your terminal's path.

  • Make sure you fully closed and reopened your terminal after running the install command.
  • On Windows, try opening a brand new Command Prompt or PowerShell window.
  • On macOS, open a new Terminal window and try az version again.

Help me troubleshoot my Azure CLI installation.

Enable Kubernetes in Docker Desktop

Docker Desktop includes a built-in single-node Kubernetes cluster. Enabling it gives you a local environment to test your deployments before pushing to the cloud.

  • Switch to the Docker Desktop window you already have open.
  • Click the gear icon in the top-right corner to open Settings.
  • In the left sidebar, click Kubernetes.
  • Check the box labeled Enable Kubernetes.
  • Click Apply & restart.

What is Kubernetes?

Kubernetes is an orchestration platform that manages your containers. It decides how many copies of your app to run, restarts crashed containers automatically, and distributes incoming traffic across them.

Docker Desktop's built-in Kubernetes gives you a miniature version of this system on your own machine. It also installs kubectl automatically, which is the command-line tool you use to interact with any Kubernetes cluster.

  • Wait for the green Kubernetes running indicator to appear in the bottom-left corner of Docker Desktop. This may take 1-2 minutes.
  • Once the green indicator is visible, verify kubectl by running this in your terminal:
kubectl version

You should see version information for both the Client and the Server. This confirms that kubectl can communicate with your local Kubernetes cluster.

kubectl command not found?

Docker Desktop installs kubectl automatically when Kubernetes is enabled. If the command is not found, make sure the green indicator is showing in Docker Desktop. If it is still starting up, wait another minute and try again.

Also close and reopen your terminal to pick up the updated PATH.

Help me get kubectl working with Docker Desktop.

Your development environment is ready. Next up, you will build the Node.js API that you will eventually deploy to Kubernetes.

Build the Node.js App

Your development environment is set up and ready to go. Now it's time to build the application that you'll containerize and deploy to Kubernetes.

You'll create a simple Node.js API using Express. This API returns system information like the hostname and uptime, which becomes especially useful when multiple copies of your app run across a Kubernetes cluster.

In this step, get ready to:
  • Create and initialize the project folder with npm.
  • Write an Express API with health check and crash endpoints.
  • Test the app locally to confirm it works before containerizing.
Create the project folder and initialize it

Every Node.js project starts with a package.json file that tracks your project's dependencies. You'll create a folder, initialize the project, and install the Express framework.

  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir k8s-node-app
cd k8s-node-app
  • Confirm the folder exists by running:
ls

You should see the k8s-node-app folder listed on your Desktop.

  • Initialize the project with npm by running:
npm init -y

What does this command do?

The npm init -y command creates a package.json file with default values. The -y flag accepts all defaults so you don't have to answer prompts one by one.

You should see a message confirming package.json was created.

  • Open the k8s-node-app folder in VS Code. In VS Code, click File in the top menu bar.
  • Click Open Folder.
  • Select the k8s-node-app folder on your Desktop, then click Open.
  • Open package.json in the VS Code Explorer sidebar. Replace all of its contents with the following:
{
  "name": "k8s-node-app",
  "version": "1.0.0",
  "description": "A 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 your project's manifest. It names your project, points to server.js as the entry point, and declares Express as a dependency.

The scripts.start field tells Node.js how to launch your app. Kubernetes and Docker will use this pattern later.

  • Save package.json by pressing Cmd+S (macOS) or Ctrl+S (Windows).
  • Install Express by running this command in your terminal:
npm install express@4.21.2

You should see output showing packages added and a confirmation message. A node_modules folder and package-lock.json file now appear in your project.

Seeing permission errors or EACCES?

Make sure you're inside the k8s-node-app folder. Run pwd (macOS/Linux) or cd (Windows) to check your current directory.

If you see a network error, check your internet connection and try again.

Help me fix this npm install error

Write the application code

Your project is initialized. Now you'll write the Express server that Kubernetes will eventually manage for you.

  • In VS Code, create a new file by clicking File in the top menu, then New File.
  • Name the file server.js and save it in the k8s-node-app folder.
  • Add the following code to set up Express and define the main route:
const express = require('express');
const os = require('os');
const app = express();
const PORT = 3000;

// Main route - returns system information as JSON
app.get('/', (req, res) => {
  res.json({
    hostname: os.hostname(),
    platform: os.platform(),
    uptime: process.uptime(),
    version: '1.0.0'
  });
});

What does this code do?

The first four lines import Express and the built-in os module, then create an Express app listening on port 3000.

The GET / route returns a JSON object with the machine's hostname, operating system, how long the process has been running, and the app version. When you have multiple pods in Kubernetes, the hostname will be different for each one, proving that requests are hitting different containers.

  • Below the main route, add the health check endpoint, crash endpoint, and server startup:
// Health check - Kubernetes uses this to know the app is alive
app.get('/health', (req, res) => {
  res.status(200).send('OK');
});

// Crash endpoint - deliberately kills the process to test self-healing
app.get('/crash', (req, res) => {
  res.send('Crashing in 1 second...');
  setTimeout(() => {
    process.exit(1);
  }, 1000);
});

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

What does this code do?

The /health route returns a simple 200 OK response. Kubernetes will ping this endpoint repeatedly to confirm your app is still running. If it stops responding, Kubernetes restarts the container automatically.

The /crash route calls process.exit(1) after one second, which kills the Node.js process. You'll use this later to prove that Kubernetes detects the crash and spins up a replacement.

Finally, app.listen starts the server on port 3000 and prints a confirmation message.

  • Save server.js.

✔️ 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

const express = require('express');
const os = require('os');
const app = express();
const PORT = 3000;

app.get('/', (req, res) => {
  res.json({
    hostname: os.hostname(),
    platform: os.platform(),
    uptime: process.uptime(),
    version: '1.0.0'
  });
});

app.get('/health', (req, res) => {
  res.status(200).send('OK');
});

app.get('/crash', (req, res) => {
  res.send('Crashing in 1 second...');
  setTimeout(() => {
    process.exit(1);
  }, 1000);
});

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

VS Code showing red squiggly lines?

If you see warnings under require, that's just VS Code suggesting ES module syntax. Your code will still run correctly with node. You can ignore these warnings.

Help me understand these VS Code warnings

Test the app locally

Before packaging your app into a container, you should confirm it runs correctly on your own machine. This saves debugging time later.

  • Start the server 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.

You should see a JSON response with your machine's hostname, platform, uptime, and version 1.0.0.

What is the hostname value?

Right now the hostname is your computer's name. Once you deploy to Kubernetes, each pod gets its own unique hostname. Refreshing the page will show different hostnames as requests hit different pods.

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

Not seeing the JSON response?

Make sure your terminal still shows Server running on port 3000. If you see an error, check that you saved server.js and that you're running the command from inside the k8s-node-app folder.

If port 3000 is already in use, stop whatever process is using it or restart your terminal.

Help me debug my Node.js server

Your app is running locally and returning system information. 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. Now it's time to package it into a format that Kubernetes can understand and deploy anywhere.

A Docker container bundles your app code, its dependencies, and a runtime environment into a single portable unit. Once containerized, your app runs identically whether it's on your laptop, a coworker's machine, or a cloud server.

In this step, get ready to:
  • Write a Dockerfile that packages your Node.js app into a container image.
  • Build and run the container to confirm it works.
Write the Dockerfile

A Dockerfile is a set of instructions that tells Docker how to build your container image. Each line adds a layer, from the base operating system all the way up to your application code.

  • In VS Code, create a new file in your k8s-node-app folder called Dockerfile (no file extension).
  • Paste the following code into Dockerfile:
# Use a lightweight Alpine-based Node.js image
FROM node:24-alpine

# Set the working directory inside the container
WORKDIR /app

# Copy dependency files and install production dependencies
COPY package.json package-lock.json ./
RUN npm ci --only=production

# Copy the application code
COPY server.js .

# Document the port the app listens on
EXPOSE 3000

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

What does this code do?

  • FROM node:24-alpine starts from a minimal Linux image with Node.js pre-installed. Alpine images are around 50MB compared to 300MB for full Debian-based images.
  • WORKDIR /app creates a folder inside the container and sets it as the working directory for all following commands.
  • COPY and RUN npm ci --only=production installs only the packages your app needs to run. Copying dependency files first means Docker can cache this layer and skip re-installing when only your app code changes.
  • EXPOSE 3000 documents which port the container listens on.
  • CMD ["node", "server.js"] tells Docker what command to run when the container starts.
  • Save Dockerfile.
  • Confirm the file appears in the VS Code Explorer sidebar alongside server.js and package.json.

Don't see the Dockerfile in your project?

  • Make sure you named the file exactly Dockerfile with a capital D and no extension. Names like dockerfile.txt or Dockerfile.docker will not work.
  • Confirm the file is at the top level of k8s-node-app, not inside a subfolder.

Why isn't Docker recognizing my Dockerfile?

✔️ Awesome, I've got everything!

Great. Double check you've saved your file, then move on to building the image.

ⓧ I'd like to double check the full code

FROM node:24-alpine

WORKDIR /app

COPY package.json package-lock.json ./
RUN npm ci --only=production

COPY server.js .

EXPOSE 3000

CMD ["node", "server.js"]
Build and run the container

With the Dockerfile written, you can now build an image and spin up a container from it. This proves your app works inside a container before you hand it off to Kubernetes.

  • Build your Docker image by running this command in the terminal (make sure you're in the k8s-node-app folder):
docker build -t k8s-node-app:v1 .
  • Wait for the build to complete. You should see a Successfully built message with an image ID at the end.

Seeing a build error?

  • Make sure Docker Desktop is running. You should see the Docker whale icon in your system tray or menu bar.
  • Check that your terminal is inside the k8s-node-app folder where the Dockerfile lives. The . at the end of the build command means "use the current directory."
  • Confirm your Dockerfile has no typos. A missing space between COPY and the file paths is a common mistake.

Why is my Docker build failing?

  • Run the container by executing this command:
docker run -p 3000:3000 k8s-node-app:v1

What does this command do?

The -p 3000:3000 flag maps port 3000 on your machine to port 3000 inside the container. Without this, the container's port would be isolated and unreachable from your browser.

  • Open your browser and visit http://localhost:3000.
  • You should see the same JSON response with hostname, platform, uptime, and version fields. Notice the hostname is now a short container ID instead of your computer's name.

Why is the hostname different?

The app is no longer running directly on your machine. It's running inside an isolated container, which has its own hostname. This is proof that your app is fully containerized.

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

App not loading in the browser?

  • If you see "port is already allocated," stop any process using port 3000 first. You may have left node server.js running from the previous step.
  • Make sure the terminal shows Server running on port 3000 after the docker run command. If it exited immediately, check for typos in your Dockerfile or server.js.

Why can't I access my containerized app at localhost:3000?

Your app is containerized and ready to go. Next up, you'll hand this container to Kubernetes and let it manage multiple copies of your app automatically.

Deploy to Local Kubernetes

Your app is packaged into a Docker container. Running containers one at a time with docker run works for testing, but in production you need something that watches your containers and responds when things go wrong.

Kubernetes is that something. In this step, you'll tell Kubernetes exactly how to run your app, then deliberately crash it to prove Kubernetes can recover on its own.

In this step, get ready to:
  • Write Kubernetes manifest files that describe how your app should run.
  • Deploy the app to your local Kubernetes cluster.
  • Test Kubernetes self-healing by crashing a pod.
Write the Kubernetes manifests

Kubernetes doesn't read your Dockerfile directly. Instead, you give it manifest files written in YAML that describe what to run and how to expose it to the network.

What is a Kubernetes manifest?

A manifest is a YAML file that declares the desired state of your application. You tell Kubernetes "I want 2 copies of my app running at all times" and Kubernetes makes it happen.

You'll define two resources in one file: a Deployment (which manages your running pods) and a Service (which routes network traffic to them).

  • In VS Code, create a new file called deployment.yaml inside your k8s-node-app folder.
  • Add the Deployment manifest by pasting this code:
apiVersion: apps/v1
kind: Deployment
metadata:
  name: k8s-node-app
spec:
  replicas: 2
  selector:
    matchLabels:
      app: k8s-node-app
  template:
    metadata:
      labels:
        app: k8s-node-app
    spec:
      containers:
        - name: k8s-node-app
          image: k8s-node-app:v1
          ports:
            - containerPort: 3000
          livenessProbe:
            httpGet:
              path: /health
              port: 3000
            initialDelaySeconds: 5
            periodSeconds: 10
          readinessProbe:
            httpGet:
              path: /health
              port: 3000
            initialDelaySeconds: 3
            periodSeconds: 5

What does this code do?

  • replicas: 2 tells Kubernetes to always keep 2 copies (pods) of your app running.
  • image: k8s-node-app:v1 points to the Docker image you built in the previous step.
  • livenessProbe hits the /health endpoint every 10 seconds. If the app stops responding, Kubernetes restarts the pod.
  • readinessProbe checks every 5 seconds whether the pod is ready to receive traffic.

You won't see a change yet. The next section adds the Service definition that exposes your app to the network.

  • Below the Deployment manifest, add a --- separator and the Service definition by pasting this code at the bottom of the file:
---
apiVersion: v1
kind: Service
metadata:
  name: k8s-node-app-service
spec:
  type: LoadBalancer
  selector:
    app: k8s-node-app
  ports:
    - protocol: TCP
      port: 80
      targetPort: 3000

What does the Service do?

  • type: LoadBalancer tells Kubernetes to assign an externally reachable address. On Docker Desktop, this maps to localhost.
  • port: 80 is the port you'll visit in your browser. targetPort: 3000 is where your Express server listens inside the container.
  • The selector matches pods with the label app: k8s-node-app, so the Service knows which pods to route traffic to.
  • Save deployment.yaml.

YAML not saving correctly?

  • YAML is whitespace-sensitive. Make sure you're using spaces (not tabs) for indentation.
  • The --- separator must be on its own line between the Deployment and Service sections.
  • Check that the file is saved as deployment.yaml (not .yml or .txt).

help me fix my deployment.yaml formatting

✔️ Awesome, I've got everything!

Great. Make sure you've saved deployment.yaml before moving on.

ⓧ I'd like to double check the full code

Here's the complete deployment.yaml file:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: k8s-node-app
spec:
  replicas: 2
  selector:
    matchLabels:
      app: k8s-node-app
  template:
    metadata:
      labels:
        app: k8s-node-app
    spec:
      containers:
        - name: k8s-node-app
          image: k8s-node-app:v1
          ports:
            - containerPort: 3000
          livenessProbe:
            httpGet:
              path: /health
              port: 3000
            initialDelaySeconds: 5
            periodSeconds: 10
          readinessProbe:
            httpGet:
              path: /health
              port: 3000
            initialDelaySeconds: 3
            periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
  name: k8s-node-app-service
spec:
  type: LoadBalancer
  selector:
    app: k8s-node-app
  ports:
    - protocol: TCP
      port: 80
      targetPort: 3000
Deploy to Docker Desktop Kubernetes

Your manifest describes what you want. Now you'll hand it to Kubernetes and let the cluster bring your app to life.

  • Switch your kubectl context to the Docker Desktop cluster by running this command:
kubectl config use-context docker-desktop

Why set the context?

If you later connect to Azure or another cluster, kubectl remembers each connection as a "context." This command ensures you're talking to your local cluster right now.

  • Deploy your app by applying the manifest:
kubectl apply -f deployment.yaml
  • Check that your pods are running:
kubectl get pods

Wait until both pods show Running in the STATUS column. This may take 10-30 seconds.

Pods stuck in a non-Running state?

  • If you see ImagePullBackOff, Kubernetes can't find the Docker image. Make sure you built the image with docker build -t k8s-node-app:v1 . in the previous step.
  • If you see CrashLoopBackOff, the container starts but immediately exits. Run kubectl logs <pod-name> to see the error output.
  • Make sure Docker Desktop's Kubernetes is enabled (green indicator in Docker Desktop settings).

help me debug why my Kubernetes pods aren't starting

  • Open your browser and visit http://localhost (port 80, the default).

You should see a JSON response with hostname, platform, uptime, and version fields. The hostname is the pod name that served your request.

Test Kubernetes self-healing

Your app has a /crash endpoint that deliberately kills the process. In a normal Docker setup, that container would stay dead. Kubernetes should detect the failure and restart it automatically.

  • In your browser, visit http://localhost/crash to kill one of your pods.
  • Immediately switch to your terminal and run:
kubectl get pods

You should see one pod with a status of Error or CrashLoopBackOff while Kubernetes automatically creates a replacement.

  • Wait about 30 seconds, then run kubectl get pods again.

Both pods should be back to Running status. The RESTARTS column will show a count of 1 or higher for the pod that crashed.

What just happened?

When your app called process.exit(1), the container stopped. Kubernetes detected the failure through the livenessProbe and automatically restarted the pod. The other pod kept serving traffic the entire time.

This is self-healing in action. You never had to manually restart anything. Kubernetes noticed the problem and fixed it.

App not accessible after the crash?

  • If http://localhost stops responding entirely, wait 30-60 seconds for Kubernetes to finish restarting the pod.
  • Run kubectl get pods to confirm at least one pod shows Running status before retrying the URL.

help me understand why my app is still down after crashing a pod

Your app is running on Kubernetes locally, and you've seen it heal itself after a crash. Next up, you'll push this same setup to Azure so your app is accessible from anywhere on the internet.

Set Up Azure and Push Your Image

Your app runs perfectly on your local Kubernetes cluster. Now it's time to move it to the cloud where anyone on the internet can reach it.

Azure Kubernetes Service (AKS) needs to pull your container image from a registry it can access. That registry is Azure Container Registry (ACR), a private Docker registry hosted in Azure. In this step, you'll log in to Azure, create the cloud resources, and push your image to ACR.

In this step, get ready to:
  • Log in to Azure and create a resource group.
  • Create an Azure Container Registry.
  • Push your Docker image to ACR.
Log in to Azure and create resources

Before you can create anything in Azure, you need to authenticate. The Azure CLI handles this by opening a browser window where you sign in with your Microsoft account.

  • Authenticate with Azure by running this command:
az login

What happens when you run this?

On Windows, a sign-in dialog appears directly. On macOS and Linux, your default browser opens an Azure sign-in page. Sign in with your Microsoft account credentials.

After signing in, your terminal displays a list of your Azure subscriptions in JSON format. If you have multiple subscriptions, Azure CLI may ask you to select one.

Browser not opening or login hanging?

If the browser-based login fails, try the device code method instead:

az login --use-device-code

This prints a code and a URL. Open the URL in any browser, enter the code, and sign in there.

help me log in to Azure CLI

Don't have an Azure account?

New Azure accounts receive $200 in free credit for 30 days. Visit azure.microsoft.com/free to create one. You can complete this project well within the free credit.

Next, you need a resource group. This is a container that holds all the Azure resources for your project in one place.

  • Create a resource group by running this command:
az group create --name k8s-project-rg --location eastus

What does this command do?

`--name k8s-project-rg` sets the resource group name. All Azure resources you create in this project will live inside this group.

`--location eastus` places the resources in the US East data center. This keeps latency low and costs consistent.

  • Confirm the output shows "provisioningState": "Succeeded" in the JSON response.

Now create your Azure Container Registry. This is where your Docker image will live so that AKS can pull it.

What is Azure Container Registry?

ACR is a private Docker registry in the cloud. Instead of pushing images to Docker Hub (which is public by default), ACR keeps your images private and close to your Azure resources for faster pulls.

  • Create your container registry by running this command:
az acr create --name [[ACR_NAME="your unique registry name e.g. k8snodeapp123"]] --resource-group k8s-project-rg --sku Basic

How do I pick my registry name?

The ACR name must be globally unique across all of Azure. It can only contain letters and numbers (no hyphens or underscores), and must be between 5 and 50 characters long.

A good pattern is your initials plus the project name plus a few numbers. For example: jdk8snode42 or k8snodeapp2026.

  • Wait for the command to finish (this takes about 30 seconds).
  • Confirm the JSON output shows your registry name and "provisioningState": "Succeeded".

Seeing a name already taken error?

ACR names are globally unique. If you get an error saying the name is not available, try a different combination with more numbers or your initials.

help me pick a unique ACR name

Push your Docker image to ACR

Your registry exists, but it's empty. You need to push the Docker image you built earlier so that AKS can pull it when you deploy.

  • Authenticate Docker with your ACR by running this command:
az acr login --name [[ACR_NAME="your unique registry name"]]
  • You should see Login Succeeded in the output.

What does this command do?

This command configures Docker to authenticate with your private ACR. Without this step, Docker would be denied permission to push images to your registry.

Docker images need a specific naming format to be pushed to a registry. You need to tag your local image with the full ACR address.

  • Tag your local image with the ACR address by running this command:
docker tag k8s-node-app:v1 [[ACR_NAME="your unique registry name"]].azurecr.io/k8s-node-app:v1

What does tagging do?

Tagging doesn't create a new image. It adds a second name (an alias) that includes the registry address. Docker uses this address to know where to push the image.

The format is <registry-name>.azurecr.io/<image-name>:<tag>.

  • Push the tagged image to ACR by running this command:
docker push [[ACR_NAME="your unique registry name"]].azurecr.io/k8s-node-app:v1
  • Watch the output as Docker uploads each layer of your image. This takes 1-2 minutes depending on your internet speed.
  • Confirm the push is complete when you see all layers showing Pushed and a final digest line.

Push denied or unauthorized error?

Make sure you ran az acr login --name <your-acr-name> successfully before attempting the push. The login token can expire, so re-run it if needed.

Also double-check that the registry name in your docker tag command matches the ACR name you created. A single typo will cause the push to fail.

help me push my Docker image to ACR

Your container image is now stored in the cloud. Next up, you'll create an AKS cluster and deploy your app to it with a public IP address anyone can access.

Deploy to Azure Kubernetes Service

Your container image is sitting in Azure Container Registry, ready to be pulled. Now it is time to create the actual Kubernetes cluster in the cloud that will run your app.

Azure Kubernetes Service (AKS) gives you a managed Kubernetes cluster where Azure handles the control plane. You only pay for the worker nodes that run your containers.

In this step, get ready to:
  • Create a 1-node AKS cluster connected to your container registry.
  • Point kubectl at the new cloud cluster and deploy your app.
  • Access your app from a public IP address on the internet.
Create the AKS cluster

AKS needs at least one worker node (a virtual machine) to schedule your pods onto. The Free tier means the control plane itself costs nothing. You only pay for the VM compute time.

  • Create a 1-node AKS cluster attached to your ACR by running this command:
az aks create --resource-group k8s-project-rg --name k8s-project-cluster --node-count 1 --generate-ssh-keys --attach-acr [[ACRNAME="your ACR name"]]

What does this command do?

  • --resource-group k8s-project-rg places the cluster in the same resource group as your ACR.
  • --name k8s-project-cluster names the cluster so you can reference it later.
  • --node-count 1 creates a single worker node to keep costs low.
  • --generate-ssh-keys auto-creates SSH keys if they do not already exist on your machine.
  • --attach-acr grants the cluster permission to pull images from your registry without manual role assignments.
  • Wait 3-5 minutes for the cluster to finish provisioning. The command prints a large JSON block when it completes.

Seeing a quota error?

If you see a message about insufficient vCPU quota, your subscription may not have enough capacity in the eastus region. Try a different region by adding --location westus2 to the command.

Help me troubleshoot my AKS cluster creation error

Connect kubectl and deploy

Your kubectl is currently pointing at the local Docker Desktop cluster. You need to switch it to your new AKS cluster so your commands go to the cloud.

  • Download the AKS cluster credentials by running this command:
az aks get-credentials --resource-group k8s-project-rg --name k8s-project-cluster

What does this command do?

This merges the AKS cluster's connection info into your local kubeconfig file. From now on, kubectl commands target k8s-project-cluster in Azure instead of docker-desktop.

Next, you need to update deployment.yaml so the image reference points to your ACR registry instead of the local Docker image.

  • In deployment.yaml, find this line inside the containers section:
          image: k8s-node-app:v1
  • Replace k8s-node-app:v1 with your full ACR image path, so the line looks like this:
          image: [[ACRNAME="your ACR name"]].azurecr.io/k8s-node-app:v1
  • Save deployment.yaml.
  • Deploy your app to AKS by running this command:
kubectl apply -f deployment.yaml

What does this command do?

kubectl apply sends your Deployment and Service definitions to the AKS cluster. Kubernetes pulls the image from ACR and starts 2 replicas of your app.

  • Confirm your pods are running by running:
kubectl get pods

You should see 2 pods with status Running. It may take a minute for the pods to pull the image and start.

Pods stuck in ImagePullBackOff?

This means the cluster cannot pull your image from ACR. Double check that the image path in deployment.yaml matches exactly what you pushed (including the .azurecr.io suffix and :v1 tag).

Also verify that the --attach-acr flag was included when you created the cluster. If you missed it, run az aks update --resource-group k8s-project-rg --name k8s-project-cluster --attach-acr followed by your ACR name to fix it.

Help me fix my ImagePullBackOff error on AKS

✔️ Awesome, I've got everything!

Great. Double check you have saved deployment.yaml with the updated image path.

ⓧ I'd like to double check the full code

Here is the complete deployment.yaml at this point:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: k8s-node-app
spec:
  replicas: 2
  selector:
    matchLabels:
      app: k8s-node-app
  template:
    metadata:
      labels:
        app: k8s-node-app
    spec:
      containers:
        - name: k8s-node-app
          image: [[ACRNAME="your ACR name"]].azurecr.io/k8s-node-app:v1
          ports:
            - containerPort: 3000
          livenessProbe:
            httpGet:
              path: /health
              port: 3000
            initialDelaySeconds: 5
            periodSeconds: 10
          readinessProbe:
            httpGet:
              path: /health
              port: 3000
            initialDelaySeconds: 3
            periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
  name: k8s-node-app-service
spec:
  type: LoadBalancer
  selector:
    app: k8s-node-app
  ports:
    - protocol: TCP
      port: 80
      targetPort: 3000
Access your live app

Your app is now running on AKS, but you need a public IP address to reach it from the internet. The LoadBalancer Service you defined requests one from Azure automatically.

  • Watch for the external IP to be assigned by running:
kubectl get service --watch

What should I see?

The EXTERNAL-IP column starts as <pending>. After 1-2 minutes, it changes to a real public IP address (for example 20.85.123.45). Once you see an IP, press Ctrl+C to stop watching.

  • Copy the external IP address from the output.
  • Open your browser and visit http:// followed by the IP address.

You should see the same JSON response you saw locally, with the hostname, platform, uptime, and version fields. The hostname now shows a Kubernetes pod name instead of your local machine name.

  • Confirm your pods are healthy on AKS by running:
kubectl get pods

You should see 2 pods with status Running, now executing on an Azure VM instead of your local machine.

External IP stuck on pending?

Azure LoadBalancer provisioning can take up to 3 minutes. If it stays on <pending> for more than 5 minutes, run kubectl describe service k8s-node-app-service and look for error events in the output.

Help me figure out why my AKS LoadBalancer has no external IP

Your app is now live on the internet, running on a managed Kubernetes cluster in Azure. Next up, you will scale your deployment and watch Kubernetes load-balance traffic across multiple pods.

Scale Your Deployment

Your app is live on Azure Kubernetes Service with 2 replicas handling traffic. But what happens when your app gets popular and needs to handle more requests?

In this step, you will scale your deployment from 2 pods to 4 and watch Kubernetes distribute traffic across them. You will also test self-healing on a live cluster by deleting a pod and watching Kubernetes replace it automatically.

In this step, get ready to:
  • Scale your deployment to 4 replicas and observe load balancing.
  • Test Kubernetes self-healing by manually deleting a pod on Azure.
Scale up replicas

Right now your deployment runs 2 pods. Scaling up adds more copies of your app so Kubernetes can spread incoming requests across them. This is called load balancing.

  • Scale your deployment to 4 replicas by running this command:
kubectl scale deployment k8s-node-app --replicas=4

You should see the output: deployment.apps/k8s-node-app scaled.

  • Confirm all 4 pods are running by running:
kubectl get pods

You should see 4 pods listed, all with status Running. It may take a few seconds for the new pods to start up.

Pods stuck in Pending?

If pods stay in Pending status, your single node may not have enough resources to schedule all 4 pods. Wait 30 seconds and run kubectl get pods again. The lightweight Node.js image should fit comfortably.

Help me figure out why my pods are stuck in Pending status

  • Open your browser and refresh your app's public URL (http://<EXTERNAL-IP>) several times.

Notice the hostname field in the JSON response changes between refreshes. Each different hostname represents a different pod serving your request.

Why does the hostname change?

Your Service distributes incoming requests across all healthy pods in the deployment. Each pod has a unique hostname (its pod name). When you refresh, the Service routes you to a different pod each time, proving traffic is being load-balanced across all 4 replicas.

Test self-healing on Azure

You tested self-healing locally in Step 4 using the crash endpoint. Now you will prove it works on a live Azure cluster by manually deleting a pod.

  • First, get the list of your running pods so you can pick one to delete:
kubectl get pods

You should see 4 pods listed. Copy the full name of any one pod (it will look something like k8s-node-app-7d9f8b6c5-abcde).

  • Delete that pod by running:
kubectl delete pod [[POD_NAME="one of the pod names from kubectl get pods"]]
  • Immediately run kubectl get pods again:
kubectl get pods

You should see one pod in a Terminating or ContainerCreating state while Kubernetes spins up its replacement. The total pod count stays at 4 because the Deployment controller immediately creates a new pod to maintain the desired replica count.

  • Refresh your app at http://<EXTERNAL-IP> in your browser.

Your app responds normally because the other 3 pods are still serving traffic. This is self-healing in action on a production cluster.

  • Wait 10-15 seconds, then run kubectl get pods one more time to confirm all 4 pods are back to Running status.

Why is this important?

In production, pods can crash for many reasons: out-of-memory errors, bugs, hardware failures. Kubernetes ensures your desired state (4 running replicas) is always maintained without manual intervention. Your users never notice the disruption.

Pod not being replaced?

Make sure you deleted a pod from the k8s-node-app deployment (the name starts with k8s-node-app-). If the new pod stays in Pending, wait 30 seconds. The node needs a moment to schedule the replacement container.

Help me troubleshoot my pod replacement

Secret mission

Zero-Downtime Rolling Update

Your app is live on Azure Kubernetes Service. But what happens when you need to deploy a new version? In production, downtime costs money and frustrates users. In this mission, you will perform a rolling update that swaps your app to a new version without dropping a single request.

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely.

The AKS cluster and ACR both incur costs while they exist. Your local Docker images and project files are free to keep.

Cost warning

The AKS Free tier control plane is free, but the underlying VM node costs money while running. ACR Basic costs $0.167/day.

If you are done with this project, delete your Azure resources to avoid ongoing charges. The Delete tab below shows you how.

Resources you used:

  • Azure Resource Group k8s-project-rg (contains your AKS cluster and ACR).
  • Local Docker images (k8s-node-app:v1, k8s-node-app:v2, and ACR-tagged copies).
  • Local project folder k8s-node-app on your Desktop.
  • Docker Desktop Kubernetes (enabled in Settings).

Keep everything running

No action needed. Choose this if you are still actively building or want to experiment further with your live AKS deployment.

  • Your app remains live at the external IP address.
  • The AKS node VM and ACR will continue to incur charges (approximately $2-5 per day combined).
  • Docker Desktop Kubernetes stays enabled for local development.
  • Your local project files and Docker images remain on disk.

Pause - I'll come back to this later

Delete your Azure resources to stop all cloud charges. Keep your local files and Docker images so you can redeploy later without rebuilding everything.

  • Delete the Azure resource group (this removes the AKS cluster and ACR in one command) by running:
  • Switch your kubectl context back to your local cluster by running:
az group delete --name k8s-project-rg --yes --no-wait
  • Switch kubectl back to your local Docker Desktop cluster by running:
kubectl config use-context docker-desktop
  • Your local k8s-node-app folder, Docker images, and Docker Desktop Kubernetes all stay intact.
  • When you are ready to redeploy, create a new resource group and AKS cluster following the same steps.

Delete - I don't want to use this again

Remove all project resources and start fresh. This deletes everything from Azure, your local machine, and Docker.

  • Delete the Azure resource group (removes AKS cluster, ACR, and all associated resources) by running:
az group delete --name k8s-project-rg --yes --no-wait
  • Remove local Docker images by running:
docker rmi k8s-node-app:v1 k8s-node-app:v2
  • Remove the ACR-tagged images by running:
docker rmi [[ACRNAME="your-acr-name"]].azurecr.io/k8s-node-app:v1 [[ACRNAME="your-acr-name"]].azurecr.io/k8s-node-app:v2
  • Delete your local project folder. On macOS/Linux, run:
rm -rf ~/Desktop/k8s-node-app
  • On Windows, run:
rmdir /s /q %USERPROFILE%\Desktop\k8s-node-app
  • Disable Docker Desktop Kubernetes by opening Docker Desktop, navigating to Settings, selecting the Kubernetes tab, unchecking Enable Kubernetes, and clicking Apply & restart.

That's a Wrap!

That's a Wrap!

Nice work! You just went from a simple Node.js file on your laptop to a live, scalable application running on Azure Kubernetes Service.

You've learned how to:

  • Built a Node.js Express API and containerized it with Docker, creating a portable image that runs anywhere.
  • Deployed to both a local Kubernetes cluster and a live AKS cluster with a public IP accessible from the internet.
  • Scaled your deployment across multiple pods and witnessed Kubernetes self-healing automatically restart crashed containers.
  • Secret Mission: Performed a zero-downtime rolling update and rollback to deploy a new version of your app without dropping a single request.

Ready to quiz yourself?