Skip to content
ohsudo, Home
Networking

Nginx reverse proxy for Node.js and Express, with HTTPS and WebSockets

Set up Nginx in front of a Node.js or Express app: a complete config with HTTPS and WebSockets, a systemd service, trust proxy for real client IPs, and common errors.

Beginner3 min readApplies to Nginx 1.18+ · Node.js 20+ · Express · Ubuntu · Debian · RHEL / Rocky / AlmaLinux

by OhSudo ·

Part of a learning path:Web servers: nginx and HTTPS in production

Your Node.js app listens on port 3000. To put it on the internet properly, you want Nginx in front: it handles HTTPS, serves as the only public port, and keeps slow clients away from your app.

To get this config with your own domain, open the Nginx Config Generator with these options.

The config

Replace example.com with your domain. On Ubuntu and Debian, save it as /etc/nginx/sites-available/example.com:

map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
return 301 https://example.com$request_uri;
}
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name www.example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
return 301 https://example.com$request_uri;
}
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
client_max_body_size 10m;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
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;
# WebSockets
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_read_timeout 1h;
}
}

1. Run the app as a service

The app has to restart after a crash or a reboot. A systemd unit does that, with nothing else to install. Save it as /etc/systemd/system/myapp.service:

[Unit]
Description=My Node.js app
After=network.target
[Service]
User=www-data
WorkingDirectory=/var/www/myapp
Environment=NODE_ENV=production
Environment=PORT=3000
ExecStart=/usr/bin/node server.js
Restart=on-failure
[Install]
WantedBy=multi-user.target
Terminal window
sudo systemctl daemon-reload
sudo systemctl enable --now myapp
curl -i http://127.0.0.1:3000/

The systemd Unit Generator writes a fuller one, with security settings. If it fails to start, see Job for service failed.

2. Listen only on localhost

Otherwise, port 3000 stays open to the internet, and visitors can go around Nginx:

app.listen(process.env.PORT, '127.0.0.1');

Check it. This should show 127.0.0.1:3000, not 0.0.0.0:3000 or *:3000:

Terminal window
sudo ss -tlnp | grep ':3000 '

3. Tell Express it’s behind a proxy

Without this, req.ip is always 127.0.0.1, req.protocol says http, and secure cookies don’t get set:

const app = express();
app.set('trust proxy', 1); // one proxy in front: Nginx

Use the number of proxies you really have. true trusts anything, so a visitor can then fake their IP with a header of their own. Rate limiters and logs depend on getting this right.

4. Get the certificate

Terminal window
sudo apt install certbot
sudo certbot certonly --standalone -d example.com -d www.example.com \
--pre-hook "systemctl stop nginx" --post-hook "systemctl start nginx"

Your DNS has to point here, and port 80 must be open. If it fails, see Certbot failed to authenticate. Certbot renews by itself, using the same hooks.

5. Turn it on

Terminal window
sudo ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/
sudo rm -f /etc/nginx/sites-enabled/default
sudo nginx -t
sudo systemctl reload nginx
curl -I https://example.com/

On RHEL, Rocky and AlmaLinux, save the file as /etc/nginx/conf.d/example.com.conf instead. SELinux also blocks Nginx from connecting to your app until you allow it:

Terminal window
sudo setsebool -P httpd_can_network_connect 1

Then open the web ports, and only those:

Terminal window
sudo ufw allow 'Nginx Full'

WebSockets and Socket.IO

The Upgrade and Connection lines in the config are what make WebSockets work. Without them, connections fall back to polling, or fail with a 400. proxy_read_timeout 1h keeps a quiet connection open, since the default closes it after 60 seconds.

If you run several Node.js processes, Socket.IO also needs sticky sessions with ip_hash in an upstream block, or its handshake fails across processes:

upstream myapp {
ip_hash;
server 127.0.0.1:3000;
server 127.0.0.1:3001;
}

Then use proxy_pass http://myapp;.

When it goes wrong

You see Means Go to
502 Bad Gateway The app isn’t running, or is on another port Nginx 502
504 Gateway Time-out The app took too long to answer Nginx 504
413 Request Entity Too Large An upload over client_max_body_size Nginx 413
ERR_TOO_MANY_REDIRECTS Your app and Nginx disagree on HTTPS Too many redirects
Address already in use Something else has port 3000 or 80 Address already in use

To see what’s on a port, the Port Lookup tells you what normally runs there.

Warning

Don’t run the app as root, or give it port 80 directly. Nginx is there so that your app can be a normal user on a port that isn’t public, and a bug in the app doesn’t become root on your server.

Found this useful? Share it.

Found a mistake? Let us know at contact@ohsudo.com.

Tools

Try it yourself

Articles

Related articles