Nginx Reverse Proxy Guide 2026 — Load Balancing, SSL, and Rate Limiting
Advertisement
Introduction
Why This Matters
Nginx handles 34% of all web server traffic. As a reverse proxy in front of Node.js, it handles SSL termination, caching of static files, rate limiting, gzip compression, and load balancing — all without changing application code. In 2026, the combination of Nginx + Let's Encrypt + Node.js remains the go-to production stack for self-hosted applications.
Installation
sudo apt update
sudo apt install nginx
sudo systemctl enable nginx
sudo systemctl start nginx
# Test config before reloading
sudo nginx -t
sudo systemctl reload nginxReverse Proxy for Node.js
# /etc/nginx/sites-available/myapp
upstream nodejs_app {
server 127.0.0.1:3000;
keepalive 64;
}
server {
listen 80;
server_name myapp.com www.myapp.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name myapp.com www.myapp.com;
ssl_certificate /etc/letsencrypt/live/myapp.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/myapp.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
# Security headers
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
add_header X-Frame-Options DENY always;
add_header X-Content-Type-Options nosniff always;
add_header Referrer-Policy strict-origin-when-cross-origin always;
# Gzip compression
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types text/plain text/css application/json application/javascript
text/xml application/xml image/svg+xml;
client_max_body_size 10M;
location / {
proxy_pass http://nodejs_app;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
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;
proxy_cache_bypass $http_upgrade;
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
# Serve Next.js static files directly from Nginx (no Node.js hop)
location /_next/static/ {
alias /app/.next/static/;
expires 1y;
add_header Cache-Control "public, immutable";
}
location /public/ {
alias /app/public/;
expires 30d;
}
}Enable the site:
sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginxLoad Balancing Across Multiple Servers
upstream api_servers {
least_conn; # Route to server with fewest active connections
server 10.0.1.10:3000 weight=3; # Gets 3x more traffic
server 10.0.1.11:3000 weight=1;
server 10.0.1.12:3000 backup; # Only used when others fail
keepalive 32;
}
server {
listen 443 ssl http2;
location /api/ {
proxy_pass http://api_servers;
proxy_http_version 1.1;
proxy_set_header Connection '';
}
}Load balancing methods:
| Method | Directive | Best For |
|---|---|---|
| Round robin | (default) | Equal servers |
| Least connections | least_conn | Long requests |
| IP hash | ip_hash | Session affinity |
| Weighted | weight=N | Mixed capacity |
Rate Limiting
http {
# Define rate limit zones
limit_req_zone $binary_remote_addr zone=api:10m rate=30r/m;
limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m;
limit_conn_zone $binary_remote_addr zone=conn:10m;
server {
location /api/ {
limit_req zone=api burst=10 nodelay;
limit_conn conn 10;
limit_req_status 429;
proxy_pass http://nodejs_app;
}
location /api/auth/login {
limit_req zone=login burst=3 nodelay;
limit_req_status 429;
proxy_pass http://nodejs_app;
}
error_page 429 /429.json;
location = /429.json {
default_type application/json;
return 429 '{"error":"Too many requests","retryAfter":60}';
}
}
}Let's Encrypt SSL (Free)
# Install Certbot
sudo apt install certbot python3-certbot-nginx
# Obtain and auto-configure SSL
sudo certbot --nginx -d myapp.com -d www.myapp.com
# Test auto-renewal
sudo certbot renew --dry-run
# Manual renewal cron (certbot auto-adds this)
0 12 * * * /usr/bin/certbot renew --quietStatic File Server for SPAs
server {
listen 80;
server_name static.myapp.com;
root /var/www/static;
# SPA fallback — serve index.html for all routes
location / {
try_files $uri $uri/ /index.html;
}
# Aggressive caching for hashed assets
location ~* \.(js|css|png|jpg|ico|woff2|svg)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# No caching for HTML
location ~* \.html$ {
expires -1;
add_header Cache-Control "no-store";
}
}Performance Tuning
# /etc/nginx/nginx.conf
worker_processes auto; # Match CPU core count
worker_connections 1024;
events {
use epoll; # Fastest event model on Linux
multi_accept on;
}
http {
sendfile on; # Efficient static file serving
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
keepalive_requests 100;
# Buffer sizing
client_body_buffer_size 16k;
client_header_buffer_size 1k;
client_max_body_size 10m;
# Access logs with timing
log_format main '$remote_addr - [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'$request_time';
access_log /var/log/nginx/access.log main buffer=16k;
error_log /var/log/nginx/error.log warn;
}Common Mistakes
- No
nginx -tbefore reloading — always validate config before applying; a syntax error will stop Nginx from reloading - Missing WebSocket headers — without
UpgradeandConnection: upgradeheaders, WebSocket connections fail silently - Too-small
client_max_body_size— the default 1MB limit causes file upload failures; set it to match your app's max upload - No rate limiting on auth endpoints — login routes without rate limits are vulnerable to brute-force attacks
- Serving Node.js static assets through Nginx without caching — always set
immutablecache headers on hashed_next/static/files
Best Practices
- Always redirect HTTP to HTTPS with a 301 permanent redirect — never serve over HTTP in production
- Use
ssl_session_cache shared:SSL:10mto enable TLS session resumption and reduce handshake overhead - Serve static files (images, CSS, JS) directly from Nginx using
alias— eliminates unnecessary Node.js overhead - Enable
gzipcompression for text-based content — reduces transfer size by 60-80% for HTML, JSON, and CSS - Monitor
nginx/error.logandnginx/access.log— they are the first place to look for 502, 503, and 504 errors
Key Takeaways
- Nginx as a reverse proxy handles SSL, compression, rate limiting, and static serving without touching application code
upstreamblocks withkeepaliveenable persistent connections from Nginx to Node.js, reducing per-request overheadleast_connis the best default load balancing method when requests have variable response times- Rate limit zones use shared memory (
10m) to track request counts across all worker processes - Let's Encrypt SSL is free, auto-renewed by Certbot, and takes under 2 minutes to configure via the
--nginxplugin try_files $uri $uri/ /index.htmlis the correct pattern for SPA routing — it falls back to index.html for all unmatched routessendfile on+tcp_nopush onenables kernel-level file sending that bypasses user space for static file serving- Setting
expires 1ywithCache-Control: immutableon content-hashed assets eliminates repeat downloads entirely
Advertisement