Nginx Reverse Proxy Setup for Node.js Apps
Your Node.js app works perfectly on localhost:3000, then someone asks you to put it on a real domain with HTTPS, and suddenly you're staring at a 502 Bad Gateway page with no idea why. A properly configured Nginx reverse proxy for Node.js is the piece that turns a raw process listening on a port into a production-ready web service — and getting it wrong is one of the most common server configuration mistakes new backend developers make.
This guide walks through exactly how to set up Nginx as a reverse proxy in front of a Node.js application, why each directive matters, and the specific mistakes that cause WebSocket disconnects, dropped headers, and mysterious timeouts in production.
Why put Nginx in front of Node.js at all?
Node.js can serve HTTP traffic directly — http.createServer() works fine on its own. So why add another layer?
- TLS termination: Nginx handles HTTPS certificates and encryption, so your Node process only ever deals with plain HTTP internally.
- Static file serving: Nginx serves images, CSS, and JS far more efficiently than Express ever will, freeing your Node event loop for actual application logic.
- Load balancing: once you run more than one Node instance (via PM2 cluster mode or separate containers), Nginx can distribute requests across them.
- Security and buffering: Nginx absorbs slow clients and malformed requests before they ever reach your application code.
- Zero-downtime deploys: you can restart your Node process while Nginx queues or retries in-flight requests.
In short: Node.js is good at running your application; Nginx is good at being the front door.
Basic reverse proxy configuration
Assume your Node.js app is running with node server.js and listening on 127.0.0.1:3000. Here's a minimal but production-viable Nginx server block:
server {
listen 80;
server_name api.example.com;
location / {
proxy_pass http://127.0.0.1:3000;
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;
}
}
Save this as /etc/nginx/sites-available/api.example.com, symlink it into sites-enabled, then test and reload:
sudo ln -s /etc/nginx/sites-available/api.example.com /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
nginx -t validates syntax before you reload — always run it first. A broken config file will otherwise take down every site Nginx serves, not just the one you're editing.
The headers people forget, and why they matter
Most broken reverse proxy setups aren't broken because of proxy_pass — they're broken because of missing headers.
X-Forwarded-ForandX-Real-IP: without these, every request in your Node.js logs shows127.0.0.1as the client IP, which is useless for debugging, rate limiting, or abuse detection. Your Express app needsapp.set('trust proxy', 1)to actually read them correctly.X-Forwarded-Proto: if your app checksreq.secureto decide whether to redirect HTTP to HTTPS, and this header is missing, you'll get an infinite redirect loop because Node never sees the request as secure.UpgradeandConnection: upgrade: skip these and every WebSocket connection (Socket.IO, nativews, GraphQL subscriptions) fails to establish, often with a generic "connection closed" error that gives no indication the problem is at the proxy layer.Host: needed if your app does virtual-hosting logic or generates absolute URLs based on the incoming host.
Dropping any one of these headers doesn't usually crash the server — it just quietly breaks a specific feature, which makes this class of bug frustrating to track down after the fact.
HTTPS with Let's Encrypt
Once the HTTP proxy works, add TLS with Certbot, which edits your Nginx config automatically:
sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d api.example.com
Certbot adds a listen 443 ssl block, wires up the certificate paths, and — critically — adds an HTTP-to-HTTPS redirect on port 80. Verify the redirect exists afterward; a stray location / block on the port 80 server can override it and leave visitors on unencrypted HTTP.
Common mistakes in production
- Proxying to
localhostinstead of127.0.0.1in a Docker or containerized setup, wherelocalhostinside the Nginx container doesn't resolve to the Node container at all — use the service name or explicit IP. - No
proxy_read_timeoutfor long-running requests. The Nginx default is 60 seconds; a report-generation or file-upload endpoint that takes longer gets killed mid-response with a 504. - Forgetting
client_max_body_size. Nginx defaults to a 1MB request body limit — any file upload larger than that returns a 413 error before your Node.js code ever runs. - Running Node as root so Nginx (also often root) can bind low ports on its behalf — unnecessary, since only Nginx needs to bind port 80/443; Node should run as an unprivileged user behind it.
- Skipping
gzipat the Nginx layer and relying oncompression()middleware in Express, which burns CPU cycles Nginx could have spent instead.
Addressing the timeout and body size limits usually looks like this:
location / {
proxy_pass http://127.0.0.1:3000;
proxy_read_timeout 120s;
client_max_body_size 20M;
# ...other proxy_set_header directives
}
Frequently Asked Questions
Does Nginx replace the need for PM2 or another process manager? No. Nginx handles incoming traffic and routing; it doesn't restart a crashed Node process or manage clustering across CPU cores. Use PM2, systemd, or a container orchestrator alongside Nginx, not instead of it.
Why do my WebSocket connections work locally but fail through Nginx?
Almost always missing proxy_http_version 1.1 combined with the Upgrade/Connection headers shown above. WebSockets require an HTTP/1.1 upgrade handshake, and Nginx defaults to HTTP/1.0 for proxied connections unless told otherwise.
Should I terminate TLS at Nginx or inside Node.js?
Terminate at Nginx. Certificate renewal, cipher configuration, and HTTP/2 support are all better handled by Nginx than by Node's built-in https module, and it keeps your application code free of certificate-management logic.
Can I run multiple Node.js apps behind one Nginx instance?
Yes — use separate server_name blocks (one per domain or subdomain) or separate location blocks with different proxy_pass targets on the same domain, each pointing to a different port.
Conclusion
A correctly configured Nginx reverse proxy is what separates a Node.js app that merely runs from one that's actually production-ready: proper header forwarding for real client IPs and HTTPS detection, WebSocket upgrade support, sane timeout and body-size limits, and TLS termination handled outside your application code. Start from the minimal server block above, add the headers your app actually relies on, run nginx -t before every reload, and treat any 502 or 504 you see in production as a signal to check the proxy configuration first, not just your Node.js logs.
0 Comments
No comments yet — be the first to share your thoughts.