You can run Express behind Nginx, but if you don’t configure it right, your Express app will think it’s running directly on localhost:3000 even when it’s receiving requests from the outside world.
Let’s see Express in action, serving requests.
Here’s a minimal Express app:
// app.js
const express = require('express');
const app = express();
const port = 3000;
app.get('/', (req, res) => {
console.log('Received request for:', req.originalUrl);
console.log('Original Host:', req.get('host'));
console.log('X-Forwarded-Host:', req.get('x-forwarded-host'));
console.log('X-Forwarded-Proto:', req.get('x-forwarded-proto'));
res.send('Hello from Express!');
});
app.listen(port, () => {
console.log(`Express app listening at http://localhost:${port}`);
});
And a basic Nginx configuration in /etc/nginx/sites-available/my-express-app:
server {
listen 80;
server_name your_domain.com;
location / {
proxy_pass http://localhost:3000;
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;
}
}
When Nginx is configured as above and you access http://your_domain.com, the Express app will log:
Received request for: /
Original Host: your_domain.com
X-Forwarded-Host: your_domain.com
X-Forwarded-Proto: http
This looks good, right? Nginx is passing along crucial information. But what if your Express app needs to generate URLs, or if it relies on the Host header for specific logic? Without the correct headers, it’s like it’s still living in its own isolated world.
The primary problem Nginx solves for Express is acting as a gateway. It handles incoming public traffic, terminates SSL (if configured), and forwards requests to your Express app running on a private port (like 3000). This shields your Express app from direct exposure and allows you to manage multiple Express apps on the same server, or even different services, all behind a single public-facing IP and domain. The key is that Nginx needs to tell Express what the original request looked like, not just what Nginx is doing internally.
The proxy_set_header directives in Nginx are your tools for this.
proxy_set_header Host $host;ensures that theHostheader sent to Express reflects the domain name the client actually used (e.g.,your_domain.com), not Nginx’s internal hostname or IP.proxy_set_header X-Real-IP $remote_addr;passes the actual client IP address.proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;builds a list of IP addresses the request has traversed, starting with the client’s.proxy_set_header X-Forwarded-Proto $scheme;tells Express whether the original client connection was HTTP or HTTPS. This is vital for generating correct redirect URLs or secure cookies.
If you are using Nginx with SSL/TLS termination (which is very common), the $scheme variable will be https if Nginx is handling the SSL, and Nginx needs to tell Express this.
Consider this Nginx config for SSL:
server {
listen 80;
server_name your_domain.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name your_domain.com;
ssl_certificate /etc/letsencrypt/live/your_domain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/your_domain.com/privkey.pem;
location / {
proxy_pass http://localhost:3000;
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; # This will be 'https'
}
}
Now, when you access https://your_domain.com, the Express app will log:
Received request for: /
Original Host: your_domain.com
X-Forwarded-Host: your_domain.com
X-Forwarded-Proto: https
This allows your Express app to correctly generate https links and set secure cookies.
If your Express app is behind multiple layers of proxies, or if you have Nginx configured in a way that strips or alters these headers, your application might not receive the correct information. For instance, if proxy_set_header X-Forwarded-Proto $scheme; is missing, and you’re using HTTPS, your Express app will think the request came in over HTTP, leading to issues with secure redirects.
The req.protocol in Express will reflect the value of X-Forwarded-Proto if it’s present, and req.secure will be true if req.protocol is https. If these headers are not set correctly by the proxy, your application’s behavior regarding security and URL generation can break.
If you’re using a tool like PM2 to manage your Express app, ensure it’s not interfering with how headers are passed. PM2 itself doesn’t typically alter these headers in a problematic way, but it’s worth checking if you’ve customized its environment or startup scripts.
The crucial point is that Express relies on these headers to understand the client’s perspective. Without them, req.headers.host might show Nginx’s internal address, and req.protocol could incorrectly be http when it should be https. This breaks anything that depends on knowing the original request’s hostname or security scheme, such as generating absolute URLs, issuing redirects, or managing session cookies.
The next step is often configuring Nginx to handle websockets, which involves additional proxy_set_header directives and proxy_http_version 1.1 and Upgrade / Connection headers.