How to Set Up a Reverse Proxy for Node.js Applications
Running a Node.js application directly on port 80 or 443 requires root privileges and lacks the security, performance, and flexibility benefits of a dedicated web server. Setting up Nginx as a reverse proxy in front of your Node.js app on your Breeze server provides SSL termination, static file serving, load balancing, and protection against common attacks.
Prerequisites
- Node.js application running on your Breeze server (typically on port 3000)
- Nginx installed (
sudo apt install nginx) - A domain name with DNS pointing to your server
Step 1: Set Up Your Node.js Application with PM2
Use PM2 to manage your Node.js process in production:
npm install -g pm2
# Start your application
pm2 start app.js --name "myapp" -i max
# Enable startup on boot
pm2 startup systemd
pm2 save
Verify the application is running:
pm2 status
curl http://localhost:3000
Step 2: Configure Nginx as a Reverse Proxy
sudo nano /etc/nginx/sites-available/myapp.conf
upstream nodejs_app {
server 127.0.0.1:3000;
keepalive 64;
}
server {
listen 80;
server_name yourdomain.com www.yourdomain.com;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name yourdomain.com www.yourdomain.com;
ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
# Serve static files directly from Nginx
location /static/ {
alias /home/user/myapp/public/;
expires 30d;
access_log off;
add_header Cache-Control "public, immutable";
}
# Proxy all other requests to Node.js
location / {
proxy_pass http://nodejs_app;
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;
# Timeouts
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
}
Step 3: Enable and Test
sudo ln -s /etc/nginx/sites-available/myapp.conf /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
Handling Trust Proxy in Express
Since Nginx terminates SSL and forwards requests, your Express application needs to trust the proxy headers:
const express = require('express');
const app = express();
// Trust the first proxy (Nginx)
app.set('trust proxy', 1);
// Now req.ip returns the real client IP
// req.protocol returns 'https' correctly
// req.hostname returns the original Host header
Load Balancing Multiple Instances
If you run multiple Node.js instances with PM2 cluster mode on different ports:
upstream nodejs_app {
least_conn;
server 127.0.0.1:3000;
server 127.0.0.1:3001;
server 127.0.0.1:3002;
server 127.0.0.1:3003;
keepalive 64;
}
The least_conn directive sends new requests to the instance with the fewest active connections, providing better distribution than round-robin for varying request durations.
Health Checks and Failover
Configure Nginx to detect unhealthy backends:
upstream nodejs_app {
server 127.0.0.1:3000 max_fails=3 fail_timeout=30s;
server 127.0.0.1:3001 max_fails=3 fail_timeout=30s;
server 127.0.0.1:3002 backup;
}
If a server fails 3 times within 30 seconds, Nginx marks it as unavailable and routes traffic to the remaining servers. The backup server only receives traffic when all primary servers are down.
Security Best Practices
- Bind Node.js to localhost — never expose the application port directly:
app.listen(3000, '127.0.0.1') - Rate limit at Nginx — use
limit_reqto prevent abuse before requests reach Node.js - Block direct IP access — add a default server block that returns 444 for requests not matching your domain
- Enable security headers — add HSTS, CSP, and X-Frame-Options in the Nginx config
- Monitor with PM2 — use
pm2 monitto watch memory and CPU usage of your Node.js processes