Kubernetes

Kubernetes for Express Developers: A Beginner-Friendly Guide

Are you a Node.js + Express developer who understands Docker and Linux, but feels overwhelmed by Kubernetes? This guide is for you. We are going to transition from standard Docker to Kubernetes step-by-step.

The Application

We are deploying a very simple Node.js + Express application packaged into a Docker image called myapp:v1.

const express = require("express");
const app = express();

app.get("/hi", (req, res) => {
    res.send("Hi from Express");
});

app.listen(3000, () => console.log("Server running"));

Part 1: Without Kubernetes

Traditionally, you would run Docker containers directly on Linux servers with a Load Balancer in front. graph TD LB[Load Balancer] –> S1[Server 1\nmyapp:v1] LB –> S2[Server 2\nmyapp:v1]

The Problem:
If Server 2 crashes, the container dies. Manual intervention is required to provision a new server, install Docker, run the container, and update the load balancer to point to the new IP.


Part 2: Introducing Kubernetes

Kubernetes is an orchestration tool that automates the deployment, scaling, and management of containers.

  • Control Plane: The “brain” making global decisions.
  • Worker Nodes: Linux servers running your apps.
  • Pod: A wrapper around your container (smallest unit in K8s).
  • Deployment: Instructions (e.g., “Keep 2 copies running”).
  • Service: Internal load balancer for your Pods.

Part 3 & 4: Deploying & Request Flow

When deployed on Kubernetes with replicas: 2, traffic flows from the browser, hits the K8s Service, and is load-balanced to the Pods across Worker Nodes. graph TD B[Browser] –>|GET /hi| S{Kubernetes Service} S –> P1[Worker 1\nPod-1] S –> P2[Worker 2\nPod-2]


Part 5: When a Node Crashes

If Worker 2 crashes, Kubernetes automatically fixes it without manual intervention.

  1. Desired State: 2 Pods
  2. Current State: 1 Pod (Node crashed)
  3. Reconciliation Loop: Control plane notices the mismatch.
  4. Scheduler: Automatically creates a new Pod on a healthy Node to reach the desired state.

Part 6: Scaling

Scaling is just changing a number in your Deployment manifest. Set replicas: 5 and the Scheduler spreads pods out: graph TD subgraph Worker 1 P1[Pod-1] P3[Pod-3] P5[Pod-5] end subgraph Worker 2 P2[Pod-2] P4[Pod-4] end


Part 7: Physical vs Logical Relationships

Understanding how the hardware maps to Kubernetes concepts: graph TD PS[Physical Server] –> WN[Worker Node\nK8s Managed] WN –> P1[Pod-1\nLogical Wrapper] P1 –> C1[Container\nmyapp:v1] WN –> P2[Pod-2] P2 –> C2[Container\nmyapp:v1]


Part 8: Real-World Analogies

  • Docker Container: A Shipping Container (holds your cargo).
  • Kubernetes Pod: A Truck Trailer (holds the shipping container).
  • Deployment: A Fleet Manager (“I need 5 trucks running!”).
  • Service: A Company Switchboard (Routes calls to available operators).

Part 9: Docker vs Kubernetes

ConceptDockerKubernetes
Build Imagedocker buildStill docker build (or similar)
Run Appdocker runDeployment (Asks cluster to run Pods)
Smallest UnitContainerPod (Contains the container)
InfrastructureDocker HostWorker Node (Part of a Cluster)
OrchestrationDocker ComposeKubernetes (Multi-machine cluster)

Part 10: Complete Request Lifecycle

Tracing a request from the user to your Express code: flowchart TD A[Browser] –>|URL| B{Service} B –>|Load balances| C[Pod\nNode-2, Port 3000] C –>|Forwards| D[Container] D –>|app.get| E[Express Route] E –>|Response| A


Part 11: Autoscaling (HPA)

Kubernetes can automatically scale your application up and down based on traffic using a Horizontal Pod Autoscaler (HPA).

To make autoscaling work, you need three things:

  1. Metrics Server: A cluster add-on that actively monitors pod CPU and memory usage.
  2. Resource Requests: Your deployment.yaml must define baseline CPU/Memory requests so Kubernetes knows what “100%” utilization means.
  3. HPA Manifest: A configuration telling Kubernetes the minimum and maximum pods allowed (e.g., min: 2, max: 10).

Pro Tip: By keeping the HPA in a separate file (e.g., k8s/hpa/hpa.yaml), you make autoscaling modular. You can deploy the base app locally without it, and selectively apply the HPA only in Production!


Bonus: Running This Locally

If you have Docker and Minikube installed, you can try this exact setup yourself!

1. Start the cluster and build the image:

minikube start
eval $(minikube docker-env)
docker build -t myapp:v1 .

2. Deploy to Kubernetes:

kubectl apply -f k8s/deployment.yaml
kubectl apply -f k8s/service.yaml

3. Test the Load Balancer (v2)
If you update your app to return the pod name using os.hostname(), you can see Kubernetes load balancing in action!

docker build -t myapp:v2 .
kubectl set image deployment/myapp-deployment express-container=myapp:v2

Triggering the Autoscaler (Load Testing)

Want to see the autoscaler in action? Open three terminal windows to watch the magic happen:

  • Terminal 1 (Watch Pods): kubectl get pods -w
  • Terminal 2 (Watch HPA): kubectl get hpa -w
  • Terminal 3 (Generate Load):
kubectl run -i --tty load-generator --rm --image=busybox:1.28 --restart=Never -- \
/bin/sh -c "while sleep 0.01; do wget -q -O- http://myapp-service/hi; echo ''; done"

The Cooldown Period: Node.js is incredibly fast. To force a scaling event, you might need to temporarily lower your HPA target to 10%. Once scaled up, if you stop the load generator, Kubernetes will wait for a 5-minute cooldown period before terminating the extra pods to prevent rapid “flapping”.

Stopping & Deleting the Cluster

If you are familiar with Docker Compose, Minikube commands will feel very similar:

  • minikube stop (Like docker compose stop): Safely shuts down the virtual machine, but saves all your data, deployments, and services. You can run minikube start tomorrow and pick up exactly where you left off.
  • minikube delete (Like docker compose down -v): Completely destroys the cluster and wipes the database. You will get a 100% clean slate the next time you start it, and you will need to run your kubectl apply commands again.