Node.js + Nginx Reverse Proxy Setup | Production Configuration Guide
이 글의 핵심
Running Node.js directly on port 80/443 in production is wrong. Nginx handles SSL termination, static files, compression, rate limiting, and routing — so Node.js only handles application logic. This guide covers a complete production-ready Nginx + Node.js setup.
Architecture
Internet → Nginx (port 80/443)
├── /api/* → Node.js (port 3000)
├── /static/* → Serve files directly (no Node.js)
└── /ws/* → Node.js WebSocket (port 3000)
Nginx handles:
- SSL termination (HTTPS → HTTP to Node.js)
- Gzip compression
- Static file serving
- Rate limiting
- Security headers
1. Install Nginx
# Ubuntu/Debian
sudo apt update && sudo apt install nginx
# CentOS/RHEL
sudo dnf install nginx
# macOS
brew install nginx
# Start and enable
sudo systemctl start nginx
sudo systemctl enable nginx
# Check status
sudo systemctl status nginx
2. Basic Reverse Proxy
# /etc/nginx/sites-available/myapp
server {
listen 80;
server_name api.example.com;
location / {
proxy_pass http://localhost:3000;
# Required proxy headers
proxy_http_version 1.1;
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;
}
}
# Enable site
sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/
# Test config
sudo nginx -t
# Reload (zero downtime)
sudo systemctl reload nginx
In Node.js, read the forwarded IP:
app.set('trust proxy', 1) // trust first proxy (Nginx)
app.get('/', (req, res) => {
console.log(req.ip) // real client IP, not 127.0.0.1
console.log(req.protocol) // 'https' even if Node sees http
})
3. HTTPS with Let’s Encrypt
# Install Certbot
sudo apt install certbot python3-certbot-nginx
# Get certificate (Certbot configures Nginx automatically)
sudo certbot --nginx -d api.example.com -d www.example.com
# Test auto-renewal
sudo certbot renew --dry-run
# Auto-renewal is set up via systemd timer or cron automatically
After Certbot, your config becomes:
server {
listen 80;
server_name api.example.com;
return 301 https://$server_name$request_uri; # redirect HTTP → HTTPS
}
server {
listen 443 ssl;
server_name api.example.com;
ssl_certificate /etc/letsencrypt/live/api.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/api.example.com/privkey.pem;
# Modern SSL settings (added by Certbot)
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
location / {
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
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;
}
}
4. Complete Production Config
# /etc/nginx/sites-available/myapp
upstream node_app {
server localhost:3000;
# Load balancing (see section 6)
# server localhost:3001;
# server localhost:3002;
keepalive 32;
}
server {
listen 80;
server_name api.example.com;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name api.example.com;
# SSL
ssl_certificate /etc/letsencrypt/live/api.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/api.example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
# Security headers
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
add_header X-Frame-Options DENY always;
add_header X-Content-Type-Options nosniff always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
# Gzip compression
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_types text/plain text/css text/xml application/json application/javascript
application/xml+rss text/javascript image/svg+xml;
# Buffer settings
proxy_buffer_size 128k;
proxy_buffers 4 256k;
proxy_busy_buffers_size 256k;
# Timeouts
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
# Static files (served by Nginx, not Node.js)
location /static/ {
alias /var/www/myapp/static/;
expires 1y;
add_header Cache-Control "public, immutable";
access_log off;
}
# API routes → Node.js
location /api/ {
proxy_pass http://node_app;
proxy_http_version 1.1;
proxy_set_header Connection ""; # for keepalive
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;
}
# WebSocket
location /ws/ {
proxy_pass http://node_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_read_timeout 86400s; # keep WebSocket open
}
# Health check (no logs)
location /health {
proxy_pass http://node_app;
access_log off;
}
# Block common attack patterns
location ~* \.(env|git|svn) {
deny all;
return 404;
}
# Rate limiting (define zone in http block)
limit_req zone=api burst=20 nodelay;
}
# /etc/nginx/nginx.conf — http block additions
http {
# Rate limit zones
limit_req_zone $binary_remote_addr zone=api:10m rate=30r/m;
limit_req_zone $binary_remote_addr zone=auth:10m rate=5r/m;
# Connection limit
limit_conn_zone $binary_remote_addr zone=addr:10m;
# Hide Nginx version
server_tokens off;
# Client body size
client_max_body_size 10m;
# Include site configs
include /etc/nginx/sites-enabled/*;
}
5. WebSocket Proxying
location /ws {
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
# WebSocket upgrade headers
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
# Pass client info
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
# Don't close the connection
proxy_read_timeout 86400s;
proxy_send_timeout 86400s;
proxy_connect_timeout 7s;
}
6. Load Balancing Multiple Node.js Instances
upstream node_cluster {
least_conn; # send to server with fewest connections (recommended)
# round_robin (default)
# ip_hash; # sticky sessions — same client → same server
server localhost:3000 weight=1;
server localhost:3001 weight=1;
server localhost:3002 weight=1;
# Health check
server localhost:3003 backup; # only used when others are down
keepalive 32; # keep connections warm
}
server {
listen 443 ssl;
# ...
location / {
proxy_pass http://node_cluster;
# ...
}
}
Run multiple Node.js processes:
# PM2 cluster mode
pm2 start app.js -i max # spawns one instance per CPU core
pm2 start app.js -i 4 # 4 instances on ports 3000-3003
7. Caching API Responses
# Define cache zone in http block
proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=api_cache:10m
max_size=1g inactive=60m use_temp_path=off;
server {
# ...
location /api/public/ {
proxy_pass http://node_app;
# Cache responses
proxy_cache api_cache;
proxy_cache_valid 200 5m; # cache 200 responses for 5 minutes
proxy_cache_valid 404 1m;
proxy_cache_use_stale error timeout updating http_500 http_502 http_503;
proxy_cache_background_update on;
proxy_cache_lock on;
# Add cache status header for debugging
add_header X-Cache-Status $upstream_cache_status;
# Don't cache if Authorization header present
proxy_cache_bypass $http_authorization;
proxy_no_cache $http_authorization;
}
}
Nginx Commands Reference
# Test config syntax
sudo nginx -t
# Reload config (zero downtime)
sudo systemctl reload nginx
# Restart (brief downtime)
sudo systemctl restart nginx
# View error logs
sudo tail -f /var/log/nginx/error.log
# View access logs
sudo tail -f /var/log/nginx/access.log
# View logs for specific site
sudo tail -f /var/log/nginx/myapp.access.log
# Check which ports Nginx is using
sudo ss -tlnp | grep nginx
Key Takeaways
| Concern | Nginx handles | Node.js handles |
|---|---|---|
| SSL/TLS | Termination | Nothing |
| Static files | Direct serve | Nothing |
| Compression | Gzip | Nothing |
| Rate limiting | Per-IP limits | Per-user limits |
| Load balancing | Upstream pool | Nothing |
| Security headers | HSTS, X-Frame | App-level |
| Application logic | Nothing | Everything |
- Trust proxy: set
app.set('trust proxy', 1)in Express to get real client IP - HTTP/2: use
listen 443 ssl http2— multiplexed connections improve performance - WebSocket: requires
UpgradeandConnectionheaders inlocationblock - Let’s Encrypt:
certbot --nginxconfigures SSL automatically, renews every 90 days