Put Nginx in Front of a Node.js App
20 Nov 2025Nginx sits in front of your Node app for one reason: Node’s HTTP server is not built to face the internet directly. TLS termination, connection buffering, and serving static files are Nginx’s job, not Express’s.
The baseline config
Assume Node is listening on 127.0.0.1:3000. Here is a config that actually holds up in production, not the four-line version from a random blog comment:
upstream node_app {
server 127.0.0.1:3000;
keepalive 64;
}
server {
listen 80;
server_name app.example.com;
location / {
proxy_pass http://node_app;
proxy_http_version 1.1;
proxy_set_header Connection "";
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_connect_timeout 5s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
}
Each line here fixes a specific failure mode. Skip any of them and you’ll hit it eventually.
Why each header matters
Host — without it, your app sees 127.0.0.1 as the host and any host-based routing or req.headers.host logic breaks silently.
X-Real-IP and X-Forwarded-For — without these, every request in your Node logs shows 127.0.0.1 as the client. Rate limiting by IP, geo lookups, and abuse tracking all become useless.
X-Forwarded-Proto — Node has no idea the request arrived over HTTPS unless you tell it. If your app redirects based on req.secure or checks this header, and you skip it, you get an infinite redirect loop once you add SSL.
Connection reuse
proxy_http_version 1.1 plus proxy_set_header Connection "" enables keepalive connections between Nginx and Node. Without this, Nginx opens a new TCP connection to your app on every request. Under load, that shows up as elevated latency and, eventually, exhausted ephemeral ports on the Nginx box. The keepalive 64 line in the upstream block is what actually holds those connections open — it’s easy to add the header directives and forget this one, and then wonder why nothing changed.
Timeouts
The default Nginx proxy timeouts are 60 seconds each, which is fine for most APIs but wrong for anything doing file uploads, long-running exports, or webhook processing. If a request legitimately takes 90 seconds, Nginx will return a 504 at 60 and your Node process will keep working on a request nobody is listening for anymore. Set proxy_read_timeout to match your slowest legitimate request, not the default.
Do not set it to something absurd like 300s globally to “be safe” — that just means a hung backend keeps a worker connection open for five minutes instead of failing fast. Scope long timeouts to a specific location block for the routes that need them:
location /api/export {
proxy_pass http://node_app;
proxy_read_timeout 300s;
}
WebSockets
If your app uses WebSockets (Socket.IO, a chat feature, live updates), the config above will break them. You need the upgrade headers:
location /socket.io/ {
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;
}
Without Upgrade and Connection: upgrade, the WebSocket handshake fails and clients silently fall back to polling, or just disconnect. You’ll see this as clients reconnecting every 30-60 seconds in your Node logs, with no error on the Nginx side because as far as Nginx is concerned, it served a normal HTTP request.
Testing before you reload
Always run this before touching the live config:
sudo nginx -t
A syntax error caught here costs you nothing. The same error caught by systemctl reload nginx failing mid-deploy costs you an outage while you figure out which of your last three edits broke it.
One gotcha with proxy_pass and trailing slashes
proxy_pass http://node_app; (no trailing slash after node_app, no path) forwards the full original URI. Add a path — proxy_pass http://node_app/api/; — and Nginx starts rewriting the URI, stripping the location prefix. Mixing these up is the single most common cause of “my API routes return 404 through Nginx but work fine when I hit Node directly.” If your routes vanish only behind the proxy, this is almost always why.