nginx reverse proxy to a Node backend
Source: r-that.com —
/etc/nginx/sites-available/r-that.comCategory: Snippet — nginx
nginx → Node proxy — the single server block that terminates HTTP on port 80 (or 443) and forwards to a Node process on localhost. Handles the common gotchas: preserving the client IP, passing the original Host, upgrading websocket connections.
Config
Section titled “Config”server { listen 80; listen [::]:80; server_name r-that.com www.r-that.com;
location / { proxy_pass http://127.0.0.1:3000; proxy_http_version 1.1;
# Preserve original request info 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 upgrade support proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade";
# Timeouts (generous, but don't hang forever) proxy_connect_timeout 5s; proxy_send_timeout 60s; proxy_read_timeout 60s; }}What each piece does
Section titled “What each piece does”proxy_pass http://127.0.0.1:3000— forward to loopback on the port the Node app listens onproxy_http_version 1.1— required for websockets and forConnection: keep-aliveefficiencyHost $host— Node app sees the original hostname, not “127.0.0.1”X-Real-IP,X-Forwarded-For— Node needs these to know the actual client IP (itsreq.ipwould otherwise be nginx’s 127.0.0.1)X-Forwarded-Proto— tells Node whether the original request was HTTPS (important for cookieSecureflags, redirect URLs)Upgrade,Connection upgrade— conditional: set by socket.io clients on websocket connections; nginx forwards them so the upgrade handshake completes
In the Node app
Section titled “In the Node app”Enable trust proxy so Express reads those forwarded headers:
app.set('trust proxy', 1); // 1 means "trust one hop" (nginx only)// Now req.ip reflects the real client; req.protocol reflects the original schemeMulti-site variant
Section titled “Multi-site variant”For multiple hostnames on the same box, repeat the server {} block with a different server_name and proxy_pass:
server { listen 80; server_name app.example.com; location / { proxy_pass http://127.0.0.1:3000; ... }}
server { listen 80; server_name admin.example.com; location / { proxy_pass http://127.0.0.1:4000; ... }}
server { listen 80; server_name static.example.com; root /var/www/static;}Gotchas
Section titled “Gotchas”$hostvs$http_host.$hostcomes from the request line (reliable);$http_hostis the raw header (can be missing). Use$host.X-Forwarded-Foraccumulates. If another proxy is in front of nginx,$proxy_add_x_forwarded_forappends rather than overwriting. This is correct — you get the full chain.proxy_set_headerinsidelocationoverrides the global set. Eachlocationthat needs forwarded headers must set them (or define them at theserverlevel).- Timeout defaults are long. nginx’s default
proxy_read_timeoutis 60s; fine for web requests, might be too short for long-running uploads or SSE. Bump per-endpoint if needed. - Buffering. nginx buffers the response by default. For SSE or streaming responses, disable:
proxy_buffering off; proxy_cache off;. - Forwarded IP spoofing. If anyone can reach nginx besides your intended clients, they can inject
X-Forwarded-Forwith a fake IP. Behind Cloudflare, useCF-Connecting-IPinstead. - Websocket upgrade +
Connection: upgradeis the single most common “websocket not working” bug. Both headers required on the proxied path. trust proxyin Express is picky about how many hops to trust.1for nginx-only. If nginx is behind Cloudflare too,2.- HTTPS at the proxy. If nginx terminates TLS (listen 443 ssl), set
X-Forwarded-Proto httpsexplicitly because$schemereads the nginx-facing scheme, not what the client sent. Actually$schemeis fine — “$scheme” evaluates to “https” when nginx terminates TLS.
See also
Section titled “See also”- snippets/nginx-subdomain-static-site — sibling pattern for static files
- patterns/cloudflare-flexible-tls-for-http-origin — what sits in front of nginx for the proxied hostnames