Skip to content

βœ… Express Load Balancing

βœ… Nginx Reverse Proxy

βœ… Multi-instance scaling

βœ… Docker (dev + prod)

βœ… Multi-stage Dockerfile

βœ… Zero-downtime deployments

🐳 Scaling Express.js with Nginx Load Balancing + Zero Downtime Deployments (Docker Guide)¢

Modern applications need to handle more users, more traffic, and require zero downtime during deployments.

One of the simplest, most powerful ways to achieve this is:

βœ” Run multiple instances of your Express.js application

βœ” Put Nginx in front as a load balancer / reverse proxy

βœ” Deploy everything using Docker

βœ” Add rolling, zero-downtime deployments

In this guide, we’ll build a fully production-ready architecture.

πŸ“ Final Project StructureΒΆ

/express-load-balance

β”œβ”€β”€ app

β”‚ β”œβ”€β”€ server.js

β”‚ └── package.json

β”œβ”€β”€ nginx

β”‚ └── default.conf

β”œβ”€β”€ Dockerfile

β”œβ”€β”€ docker-compose.dev.yml

└── docker-compose.prod.yml

🧩 Step 1: Express.js Application (Cluster-Friendly)¢

To identify which instance served a request, we log the container hostname.

app/server.jsΒΆ

const  express  =  require("express");

const  os  =  require("os");

const  app  =  express();


app.get("/", (req, res) => {

res.send(`Response from instance: ${os.hostname()}`);

});


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


app.listen(3000, () => {

console.log("Express instance running on port 3000");

}); 

πŸ“¦ Step 2: package.jsonΒΆ

{

"name": "express-scaled",

"version": "1.0.0",

"scripts": {

"start": "node server.js",

"dev": "nodemon server.js"

},

"dependencies": {

"express": "^4.19.2"

},

"devDependencies": {

"nodemon": "^3.0.0"

}

}

πŸ—οΈ Step 3: Multi-Stage Dockerfile (Dev + Prod)ΒΆ

DockerfileΒΆ

# ---------------------
# BASE STAGE
# ---------------------

FROM node:18-alpine AS base

WORKDIR /app  

# ---------------------
# DEVELOPMENT STAGE
# ---------------------

FROM base AS development

COPY app/package*.json ./

RUN npm install

COPY app .

CMD ["npm", "run", "dev"] 

# ---------------------
# PRODUCTION BUILD STAGE
# ---------------------

FROM base AS build

COPY app/package*.json ./

RUN npm install --only=production

COPY app .

# ---------------------
# PRODUCTION RUNTIME
# ---------------------

FROM node:18-alpine AS production

WORKDIR /app

COPY --from=build /app /app

CMD ["npm", "start"]  

πŸ›‘οΈ Step 4: Nginx Load Balancer ConfigurationΒΆ

Nginx distributes traffic across all Express instances.

nginx/default.confΒΆ

upstream express_cluster {

server app1:3000;

server app2:3000;

server app3:3000;

}


server {

listen 80;


location / {
proxy_pass http://express_cluster;  

proxy_http_version 1.1;

proxy_set_header Upgrade $http_upgrade;

proxy_set_header Connection "upgrade";

proxy_set_header Host $host;

}

} 

πŸ› οΈ Step 5: Docker Compose for Development (Hot Reload)ΒΆ

docker-compose.dev.ymlΒΆ

version: "3.8"  

services:

app1:

build:

context: .

target: development

container_name: express_app1

volumes:

- ./app:/app

- /app/node_modules

expose:

- "3000"


app2:

build:

context: .

target: development

container_name: express_app2

volumes:

- ./app:/app

- /app/node_modules

expose:

- "3000"



app3:

build:

context: .

target: development

container_name: express_app3

volumes:

- ./app:/app

- /app/node_modules

expose:

- "3000"



nginx:

image: nginx:alpine

container_name: nginx_dev

ports:

- "80:80"

volumes:

- ./nginx/default.conf:/etc/nginx/conf.d/default.conf

depends_on:

- app1

- app2

- app3  

Run:

docker compose -f docker-compose.dev.yml up --build  

Refresh browser multiple times:

http://localhost 

You'll see different instance names β€” load balancing works πŸŽ‰

🏭 Step 6: Docker Compose for Production (Scalable)¢

Production uses replicas instead of manually listing app1/app2/app3.

docker-compose.prod.ymlΒΆ

version: "3.8"



services:

app:

build:

context: .

target: production

expose:

- "3000"

deploy:

replicas: 3

healthcheck:

test: ["CMD", "wget", "-qO-", "http://localhost:3000/health"]

interval: 5s

timeout: 3s

retries: 5



nginx:

image: nginx:alpine

ports:

- "80:80"

volumes:

- ./nginx/default.conf:/etc/nginx/conf.d/default.conf

depends_on:

- app  

Deploy:

docker compose -f docker-compose.prod.yml up --build -d  

πŸ”„ Zero-Downtime Deployments with Nginx + DockerΒΆ

The best part of this architecture is the ability to deploy new versions without taking the app offline.

Here’s how it works:

  1. New containers (v2) start

  2. Old containers (v1) continue serving traffic

  3. Health checks confirm v2 is ready

  4. Nginx begins routing to v2

  5. Old containers shut down

  6. Users experience zero downtime

This is the same pattern used in AWS ECS, Kubernetes, and Nginx ingress controllers.

πŸ› οΈ Zero-Downtime Deployment WorkflowΒΆ

1. Build a new versionΒΆ

docker build -t express-app:v2 . 

2. Update production compose fileΒΆ

services:

app:

image: express-app:v2

deploy:

replicas: 3  

3. Apply updateΒΆ

docker compose -f docker-compose.prod.yml up -d  

Docker automatically:ΒΆ

  • launches new containers

  • waits for health checks

  • gracefully shuts down old instances

Result β†’ zero dropped requests.

πŸ§ͺ Why Health Checks MatterΒΆ

The /health endpoint ensures Nginx only sends traffic to containers that are fully ready.

Express routeΒΆ

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

Docker health checkΒΆ

healthcheck:

test: ["CMD", "wget", "-qO-", "http://localhost:3000/health"]

interval: 5s

timeout: 3s

retries: 5  

This prevents broken deployments.

πŸ“Š Why This Architecture ScalesΒΆ

Component Role
Express.js replicas Serve API requests faster
Nginx Load balancing + reverse proxy
Docker Compose Manages multi-container environment
Health checks Prevents bad deployments
Replicas Horizontal scaling

This approach handles:ΒΆ

  • Traffic spikes

  • Rolling deployments

  • Fault-tolerance

  • Microservice scaling

  • Cloud-ready orchestration


πŸŽ‰ Final ThoughtsΒΆ

You now have a full production architecture:

βœ” Multiple Express instances

βœ” Nginx load balancer

βœ” Dockerized dev + prod workflows

βœ” Rolling zero-downtime deployments

βœ” Multi-stage builds

βœ” Scalable API cluster

This is the same pattern used by big companies before moving to Kubernetes.