How to Set Up Nginx for a Node.js Application

How to Set Up Nginx for a Node.js Application

How to Set Up Nginx for a Node.js Application

Node.js applications are a bit different from PHP or Java apps in one important way: Node can serve HTTP requests directly, with no separate application server required. So the first question people usually ask me is: “if Node can already handle HTTP requests on its own, why do I need Nginx at all?” It’s a fair question, and I asked it myself the first time I deployed a Node app. The answer is that Node being capable of serving traffic directly doesn’t mean it’s the best thing to expose to the public internet. Here’s how I set it up, and why.

Why Put Nginx in Front of Node.js?

Node’s built-in HTTP server is single-threaded per process and isn’t optimized for things like TLS termination, static file caching, or handling large numbers of slow client connections. Nginx does all of that extremely efficiently and lets your Node process focus purely on running your application logic. Specifically, I use Nginx to:

Requirements

Install PM2 globally:

sudo npm install -g pm2

Start your app with PM2 instead of running it manually:

cd /var/www/mynodeapp
pm2 start app.js --name mynodeapp
pm2 save
pm2 startup

The pm2 startup command prints a systemd command you need to run once to make PM2 (and your app) survive server reboots. Confirm your app is running:

curl http://127.0.0.1:3000
pm2 status

Step 1: Create the Nginx Server Block

sudo nano /etc/nginx/sites-available/mynodeapp
server {
    listen 80;
    server_name mynodeapp.example.com;

    access_log /var/log/nginx/mynodeapp.access.log;
    error_log /var/log/nginx/mynodeapp.error.log;

    location / {
        proxy_pass http://127.0.0.1:3000;
        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;
    }

    location /static/ {
        alias /var/www/mynodeapp/public/;
        expires 30d;
        access_log off;
    }
}

The Upgrade/Connection headers here matter a lot for Node apps specifically — a huge number of Node applications use WebSockets (Socket.IO, native ws, real-time dashboards, chat apps), and without these two headers set correctly, WebSocket upgrade requests fail silently and fall back to polling, or just break outright.

Step 2: Enable the Site

sudo ln -s /etc/nginx/sites-available/mynodeapp /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

Step 3: Reading Forwarded Headers in Express

If your app needs to know the real client IP or whether the original request was HTTPS (for secure cookies, for example), tell Express to trust the proxy:

app.set('trust proxy', 1);

Without this, req.ip will show Nginx’s local IP instead of the real client IP, and req.secure won’t correctly reflect HTTPS even though Nginx is terminating TLS.

Scaling with PM2 Cluster Mode and Nginx Load Balancing

Because Node is single-threaded, one process only uses one CPU core. PM2’s cluster mode spins up one process per core automatically:

pm2 start app.js --name mynodeapp -i max

-i max tells PM2 to fork as many instances as there are CPU cores, and it automatically load-balances between them on the same port — so in many cases, you don’t even need Nginx’s upstream block for this, since PM2 handles it internally.

That said, if you’re running multiple Node apps or want more control (say, spreading instances across different ports for isolation), you can still use Nginx’s upstream for balancing:

upstream node_backend {
    least_conn;
    server 127.0.0.1:3000;
    server 127.0.0.1:3001;
    server 127.0.0.1:3002;
}

server {
    listen 80;
    server_name mynodeapp.example.com;

    location / {
        proxy_pass http://node_backend;
        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;
    }
}

Adding HTTPS

sudo apt install certbot python3-certbot-nginx -y
sudo certbot --nginx -d mynodeapp.example.com

Certbot updates your server block automatically and sets up a redirect from HTTP to HTTPS. Certificate renewal is handled by a systemd timer Certbot installs for you.

Testing Your Setup

  1. sudo nginx -t — validate configuration syntax
  2. pm2 status — confirm your Node process(es) are online
  3. curl -I http://mynodeapp.example.com — confirm you get a response
  4. If using WebSockets, test the upgrade explicitly: open your app in a browser and check the Network tab for a 101 Switching Protocols response on the WebSocket connection
  5. pm2 logs mynodeapp — watch logs live while testing to catch errors immediately

Troubleshooting Common Issues

502 Bad Gateway — Your Node process crashed or isn’t listening on the port Nginx expects. Check pm2 status and pm2 logs mynodeapp for a stack trace.

WebSocket connections dropping or falling back to polling — Confirm the Upgrade and Connection headers are present exactly as shown, and that proxy_http_version 1.1; is set (WebSockets require HTTP/1.1, and Nginx defaults to 1.0 for proxied connections unless you explicitly set this).

App works locally but not through Nginx — Almost always a proxy header issue. Double check proxy_pass points to the exact port your Node app is listening on.

Node app restarts wipe out in-memory session data — If you’re storing sessions in memory and running multiple PM2 instances, users will get logged out randomly as requests bounce between processes. Move sessions to Redis (connect-redis with express-session) so all instances share the same session store.

Static assets not updating after deploy — Check the expires 30d; caching directive; browsers may be aggressively caching old versions. Consider cache-busting filenames (e.g., via a build hash) for static assets that change on deploy.

Security Considerations

Performance Tips

Real-World Use Case

I’ve run a real-time notification service in Node using Socket.IO, deployed with PM2 in cluster mode behind Nginx. The WebSocket upgrade headers were the single most important part of that config — without them, the app silently fell back to long-polling and lost the low-latency behavior the whole feature depended on. Once the headers were correct, Nginx handled TLS and thousands of concurrent WebSocket connections without breaking a sweat, while PM2 kept four Node processes running across the server’s CPU cores.

Best Practices Recap

Node and Nginx pair well together specifically because they don’t try to do each other’s job — Nginx handles the messy realities of the public internet, and Node just runs your application code. Get the proxy headers right, especially the WebSocket ones if you need them, and the rest of this setup is genuinely straightforward.

Exit mobile version