Node.js + Nginx Reverse Proxy Setup
이 글의 핵심
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
Frequently Asked Questions (FAQ)
Q. When would I use this in practice?
A. 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.
Q. What should I read before this?
A. Follow the previous article or related articles links at the bottom of each post to learn in sequence.
Q. Where can I study this more deeply?
A. Check cppreference and the relevant library’s official documentation. The reference links at the end of the article are also worth using.
Related Articles (Internal Links)
Other articles related to this topic.
- Docker & Kubernetes Beginners Guide
- Docker Compose Complete Guide | Multi-Container Apps
- GitHub Actions CI/CD Guide | Workflows· Secrets
Keywords Covered in This Article (Related Search Terms)
This article covers Nginx, Node.js, DevOps, HTTPS, SSL, Backend.