This project demonstrates how to use Docker, NGINX, and Node.js to create a scalable application architecture with load balancing and reverse proxy capabilities.
docker-containers/
├── app1/
│ ├── server.js # Express app (Port 3001)
│ ├── package.json
│ └── Dockerfile
├── app2/
│ ├── server.js # Express app (Port 3002)
│ ├── package.json
│ └── Dockerfile
├── app3/
│ ├── server.js # Express app (Port 3003)
│ ├── package.json
│ └── Dockerfile
├── nginx/
│ ├── nginx.conf # NGINX reverse proxy configuration
│ └── Dockerfile
└── docker-compose.yml # Orchestration configuration
NGINX acts as a reverse proxy, sitting in front of multiple Node.js applications. When a client makes a request to NGINX (port 80), NGINX forwards the request to one of the backend servers.
NGINX uses round-robin load balancing by default:
- Request 1 → App1
- Request 2 → App2
- Request 3 → App3
- Request 4 → App1 (cycle repeats)
All containers are connected via a Docker network (nodejs_network), allowing them to communicate using service names as hostnames (e.g., http://app1:3001).
Each service has health checks configured to ensure containers are running properly. NGINX can automatically skip unhealthy backends.
- Docker Desktop installed and running
- Docker Compose (usually included with Docker Desktop)
cd docker-containers
docker-compose up --buildThis command will:
- Build Docker images for all services
- Create a custom bridge network
- Start all containers (NGINX proxy + 3 Node.js apps)
- Run health checks
docker-compose psYou should see 4 containers running:
- nginx-proxy (Port 80)
- nodejs-app1 (Port 3001)
- nodejs-app2 (Port 3002)
- nodejs-app3 (Port 3003)
1. Test round-robin load balancing:
# Run multiple requests and observe which app responds
curl http://localhost
curl http://localhost
curl http://localhostEach response will show a different hostname and app name, proving load distribution.
2. Check health of all backends:
curl http://localhost/health3. Get system information:
curl http://localhost/api/dataThis will show CPU count, memory, and platform info for each app.
1. Single request to /compute endpoint:
curl "http://localhost/compute?iterations=100000000"2. Load test with Apache Bench (included with most systems):
# 100 requests with 10 concurrent connections
ab -n 100 -c 10 http://localhost/
# More intense: 1000 requests with 50 concurrent connections
ab -n 1000 -c 50 http://localhost/3. Load test with curl (alternative method):
# Run 20 requests in parallel
for i in {1..20}; do curl http://localhost & done; wait4. Monitor load distribution in real-time:
# In one terminal, watch NGINX status
watch curl http://localhost/nginx_status
# In another terminal, run load tests
ab -n 1000 -c 20 http://localhost/upstream nodejs_backend {
server app1:3001; # Backend 1
server app2:3002; # Backend 2
server app3:3003; # Backend 3
}Key directives:
upstream- Defines a group of backend serversserver app1:3001- Each backend server (uses DNS from Docker network)proxy_pass- Routes request to upstream groupproxy_set_header- Preserves client informationproxy_read_timeout- Timeout for responses from backend
Modify nginx/nginx.conf:
upstream nodejs_backend {
server app1:3001 weight=3; # Gets 3x more traffic
server app2:3002 weight=1;
server app3:3003 weight=1;
}Replace the upstream block:
upstream nodejs_backend {
least_conn; # Routes to server with least active connections
server app1:3001;
server app2:3002;
server app3:3003;
}upstream nodejs_backend {
ip_hash; # Same client always goes to same server
server app1:3001;
server app2:3002;
server app3:3003;
}Each service in docker-compose.yml includes:
- build: Specifies Dockerfile and context
- environment: Sets environment variables
- networks: Connects to custom bridge network
- healthcheck: Monitors container health
- restart: Auto-restart policy
- volumes: (NGINX only) Mounts config file
Stop all containers:
docker-compose downStop without removing volumes:
docker-compose down --volumesView logs:
# All services
docker-compose logs
# Specific service
docker-compose logs nginx
docker-compose logs app1
# Follow logs in real-time
docker-compose logs -fRestart a service:
docker-compose restart app1Scale a service (manually):
docker-compose up -d --no-deps --build app1 app1 app1
# This doesn't work directly - you'd need to use docker-compose multiple times
# Or modify docker-compose.yml to add more instancesTo add more Node.js applications:
- Create
app4/,app5/, etc. directories withserver.js,package.json, andDockerfile - Update
docker-compose.ymlto include new services - Update
nginx/nginx.confto add new upstream servers:
upstream nodejs_backend {
server app1:3001;
server app2:3002;
server app3:3003;
server app4:3004;
server app5:3005;
}Containers communicate using service names:
app1resolves to app1's container IP- NGINX config:
proxy_pass http://app1:3001
External clients access through NGINX on localhost:80:
- Browser:
http://localhost/ - CURL:
curl http://localhost
Monitor resource usage in real-time:
docker statsThis shows CPU%, memory usage, I/O, and network stats for each container.
View connection statistics:
curl http://localhost/nginx_statusOutput shows:
- Active connections
- Server connections (accepts, handled, requests)
- Reading, Writing, Waiting states
# Check logs
docker-compose logs app1
# Check if port is already in use
lsof -i :3001- Verify containers are running:
docker-compose ps - Check network:
docker network ls - Test connectivity:
docker exec nginx-proxy ping app1
- Verify NGINX config:
docker exec nginx-proxy nginx -t - Check upstream directive in config file
- Ensure all backends are healthy:
curl http://localhost/health
-
NGINX
- Reverse Proxy: Routes requests to backend servers
- Load Balancing: Distributes traffic
- Static file serving
- SSL/TLS termination
-
Docker
- Containerization: Packaging apps with dependencies
- Docker Compose: Multi-container orchestration
- Networks: Container communication
- Health Checks: Automated monitoring
-
Node.js/Express
- HTTP server
- Request routing
- Middleware pattern
- Experiment with different load balancing algorithms (weighted, least connections, IP hash)
- Add SSL/TLS with self-signed certificates
- Implement caching in NGINX to improve performance
- Add logging and monitoring (ELK stack, Prometheus)
- Deploy to Kubernetes for advanced orchestration
- Implement session persistence if you have stateful apps
- Add rate limiting in NGINX to prevent abuse
Use NGINX as an API gateway in front of multiple microservices.
Use split_clients directive to route traffic to different versions.
Add caching directives to NGINX for improved performance.
- SSL/TLS encryption
- Rate limiting
- IP whitelisting
- Request validation
| File | Purpose |
|---|---|
app*/server.js |
Express.js application with health checks and endpoints |
app*/Dockerfile |
Multi-stage build for optimized Node.js images |
nginx/nginx.conf |
Upstream definition, load balancing, and proxy settings |
nginx/Dockerfile |
Builds NGINX image with custom config |
docker-compose.yml |
Orchestrates all services with networking and health checks |
Happy Learning! 🚀