Reverse Proxy with Nginx
Node should not be exposed directly to the internet. A reverse proxy sits in front and handles the work Node isn't good at: TLS termination (your certificate lives at Nginx, Node speaks plain HTTP internally), serving static files at OS speeds without touching Node, request buffering (Nginx absorbs slow clients so your Node workers are freed instantly), gzip compression, rate limiting, and routing traffic across multiple backend instances. Nginx is the most widely used reverse proxy for Node, and once you understand its configuration model — server blocks, location blocks, the proxy_pass directive — the rest follows.
Why Node needs a proxy in front
Concern | Node alone | With Nginx in front |
|---|---|---|
TLS / HTTPS | Manageable but complex | Nginx terminates; Node handles plain HTTP internally |
Slow clients | Worker thread held until client finishes receiving | Nginx buffers; Node worker freed immediately |
Static files | Served via Node (slow, ties up event loop) | Served directly from disk at full OS speed |
Port 80/443 | Requires root or | Nginx binds; Node runs on an unprivileged port |
Multiple apps | Can't share port 80/443 | Nginx routes by domain/path to any backend |
A minimal proxy config
/etc/nginx/sites-available/myapp
server {
listen 80;
server_name api.example.com;
return 301 https://$host$request_uri; # redirect all 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;
location / {
proxy_pass http://localhost:3000; # forward to Node
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade; # needed for WebSocket upgrades
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;
}
}Static files and gzip
server {
# ...
# Serve static files directly from disk — never hit Node:
location /static/ {
root /var/www/myapp;
expires 1y;
add_header Cache-Control "public, immutable";
}
# Compress text responses before sending to the client:
gzip on;
gzip_types text/plain application/json application/javascript text/css;
gzip_min_length 1024;
location / {
proxy_pass http://localhost:3000;
}
}Load balancing across multiple Node instances
# If running separate processes (e.g. on multiple servers) rather than cluster mode:
upstream node_backend {
least_conn; # route to instance with fewest active connections
server 127.0.0.1:3001;
server 127.0.0.1:3002;
server 127.0.0.1:3003;
keepalive 32; # keep connections to upstreams warm
}
server {
location / {
proxy_pass http://node_backend;
}
}