Nginx Reverse Proxy Setup for Node.js Apps

September 19, 2026 · 1 views
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-For and X-Real-IP: without these, every request in your Node.js logs shows 127.0.0.1 as the client IP, which is useless for debugging, rate limiting, or abuse detection. Your Express app needs app.set('trust proxy', 1) to actually read them correctly.
  • X-Forwarded-Proto: if your app checks req.secure to 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.
  • Upgrade and Connection: upgrade: skip these and every WebSocket connection (Socket.IO, native ws, 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

  1. Proxying to localhost instead of 127.0.0.1 in a Docker or containerized setup, where localhost inside the Nginx container doesn't resolve to the Node container at all — use the service name or explicit IP.
  2. No proxy_read_timeout for 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.
  3. 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.
  4. 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.
  5. Skipping gzip at the Nginx layer and relying on compression() 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.

#nodejs #devops #nginx #reverse-proxy #https #server-configuration
Share this article:

0 Comments

No comments yet — be the first to share your thoughts.

Leave a comment

Never published.