Building a Secure and Scalable Node.js App with Nginx as a Reverse Proxy and Load Balancer
subtitle: "How to deploy containerized Node.js replicas behind Nginx with HTTPS and automatic HTTP-to-HTTPS redirection"
tags: ["Node.js", "Nginx", "Docker", "DevOps", "Load Balancing", "Reverse Proxy"]
Introduction
In this post, we’ll build a mini DevOps project that demonstrates how to use Nginx as a reverse proxy and load balancer for a containerized Node.js application.
We’ll deploy three Node.js replicas, serve them securely via HTTPS, and configure automatic HTTP-to-HTTPS redirection. This setup ensures:
- High availability through load balancing
- Secure traffic routing with SSL/TLS
- Seamless horizontal scalability
Architecture Overview
Here’s a quick breakdown of what we’ll build:
Client → Nginx (Reverse Proxy + Load Balancer)
↳ Node.js App 1 (Container)
↳ Node.js App 2 (Container)
↳ Node.js App 3 (Container)
Nginx will distribute requests evenly among the Node.js containers and handle SSL termination.
Step 1: Create the Node.js Application
Let’s start by creating a simple Express.js app.
mkdir node-nginx-demo
cd node-nginx-demo
npm init -y
npm install express
Then create a file app.js:
const express = require("express");
const app = express();
const port = 3000;
app.get("/", (req, res) => {
res.send(`Hello from Node.js container! Hostname: ${process.env.HOSTNAME}`);
});
app.listen(port, () => {
console.log(`App running on port ${port}`);
});
Step 2: Dockerize the Application
Create a Dockerfile:
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 3000
CMD ["node", "app.js"]
Then build the image:
docker build -t node-demo-app .
Now create three containers (replicas):
docker run -d --name app1 -p 3001:3000 node-demo-app
docker run -d --name app2 -p 3002:3000 node-demo-app
docker run -d --name app3 -p 3003:3000 node-demo-app
Each app instance is accessible on ports 3001, 3002, and 3003.
Step 3: Configure Nginx as Reverse Proxy and Load Balancer
Create an nginx.conf file:
events {}
http {
upstream node_app {
server app1:3000;
server app2:3000;
server app3:3000;
}
server {
listen 80;
server_name _;
# Redirect all HTTP traffic to HTTPS
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name _;
ssl_certificate /etc/ssl/certs/selfsigned.crt;
ssl_certificate_key /etc/ssl/private/selfsigned.key;
location / {
proxy_pass http://node_app;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
}
Step 4: Generate a Self-Signed SSL Certificate
For testing, generate a certificate inside your project:
mkdir ssl
cd ssl
openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout selfsigned.key -out selfsigned.crt -subj "/C=KE/ST=Nairobi/L=Nairobi/O=DevOps/CN=localhost"
Step 5: Create a Docker Compose File
Let’s manage all services together using Docker Compose.
Create a docker-compose.yml:
version: '3'
services:
app1:
build: .
app2:
build: .
app3:
build: .
nginx:
image: nginx:latest
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- ./ssl:/etc/ssl/:ro
depends_on:
- app1
- app2
- app3
Then run everything:
docker-compose up -d
Step 6: Test the Setup
Visit:
- http://localhost → will automatically redirect to https://localhost
- https://localhost → you’ll see “Hello from Node.js container!” and the hostname changes on refresh — showing Nginx load balancing across your replicas.
Step 7: Verify Load Balancing
Run:
docker logs app1 | tail -n 2
docker logs app2 | tail -n 2
docker logs app3 | tail -n 2
You’ll notice that requests are distributed evenly across all containers — confirming load balancing works correctly.
Key Takeaways
- Nginx Reverse Proxy: Hides backend details and distributes load.
- Load Balancing: Ensures high availability and reliability.
- SSL/TLS: Secures communication via HTTPS.
- Docker Compose: Simplifies multi-container orchestration.
This setup mimics a real-world architecture where containerized apps run behind a reverse proxy with SSL termination — a fundamental DevOps skill.
Conclusion
You’ve just built a secure, scalable, and production-like environment using Node.js, Nginx, and Docker.
This project is a great foundation to explore:
- Scaling with Kubernetes
- Automated SSL with Let’s Encrypt
- Continuous deployment pipelines for app updates
If you enjoyed this walkthrough, follow me for more DevOps hands-on projects and cloud automation tutorials.
Written by Gilbert Mutai — DevOps Engineer | Cloud Support Engineer