DeployEasy
NginxBeginner

Nginx Reverse Proxy for Node.js Applications

Configure an Nginx server block for Node.js, Next.js, or NestJS with forwarded headers, WebSockets, HTTPS, timeouts, and safe reloads.

· 5 min read· 934 words
Table of contents

Nginx Reverse Proxy for Node.js Applications

An Nginx reverse proxy sits in front of a Node.js, Next.js, or NestJS application. It accepts requests on ports 80 and 443, then forwards them to an application listening on a private port such as 3000.

Browser → Nginx :443 → Node.js :3000

This keeps the application port private and gives you one place to manage HTTPS, redirects, request headers, upload limits, timeouts, access logs, and multiple domains.

Prerequisites

Before writing the server block, make sure:

  • The domain’s DNS record points to the VPS.
  • The Node.js application is listening on 127.0.0.1:3000 or a private Docker network.
  • Nginx is installed and the firewall allows ports 80 and 443.
  • The application has a health endpoint if possible.

Check the upstream directly:

curl -v http://127.0.0.1:3000/health
ss -lntp | grep :3000

If this request fails, fix the application or container first. Nginx cannot proxy to a process that is not listening.

Install Nginx

sudo apt update
sudo apt install -y nginx
sudo systemctl enable --now nginx
sudo ufw allow 'Nginx Full'

Verify the default service:

sudo nginx -t
curl -I http://127.0.0.1

Create a server block

Create a file in sites-available:

sudo nano /etc/nginx/sites-available/app.example.com

Use this HTTP configuration while validating the upstream:

server {
    listen 80;
    listen [::]:80;
    server_name app.example.com www.app.example.com;

    client_max_body_size 20m;

    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;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_read_timeout 60s;
    }
}

Enable it and disable the default site if it is no longer needed:

sudo ln -s /etc/nginx/sites-available/app.example.com /etc/nginx/sites-enabled/app.example.com
sudo rm /etc/nginx/sites-enabled/default
sudo nginx -t
sudo systemctl reload nginx

Use nginx -t before every reload. A failed test should stop the deployment, not be followed by a forced restart.

Understand the important directives

server_name selects the virtual host. proxy_pass selects the upstream. The forwarded headers tell the application which host, protocol, and client address were used by the original request.

The trailing slash in proxy_pass changes URI rewriting behavior. For a simple Node.js application, proxy_pass http://127.0.0.1:3000; keeps the incoming URI intact. Test the exact paths your application serves before changing it.

proxy_set_header Upgrade and Connection are needed by many WebSocket applications. If the application does not use WebSockets, they are harmless in this basic configuration but can be made conditional in a larger setup.

Add HTTPS with Certbot

After DNS resolves to the VPS and the HTTP site responds, install Certbot:

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

Choose the HTTP-to-HTTPS redirect when prompted. Check automatic renewal without changing the live certificate:

sudo certbot renew --dry-run
systemctl status certbot.timer

Do not request certificates before DNS and port 80 are correct; repeated failed requests can hit the certificate authority’s rate limits.

Node.js and Docker upstreams

For a PM2 application, bind Node.js to localhost and let PM2 keep the process alive:

pm2 start dist/server.js --name app
pm2 save
pm2 startup

For Docker Compose, Nginx on the host can proxy to a published loopback port:

services:
  web:
    image: ghcr.io/example/app:sha-abc123
    ports:
      - "127.0.0.1:3000:3000"

Avoid publishing the application on 0.0.0.0:3000 unless there is a specific reason. Keeping it on loopback prevents direct public access around Nginx.

Troubleshoot 502 Bad Gateway

Check from the inside out:

curl -v http://127.0.0.1:3000/health
docker compose ps
sudo nginx -t
sudo tail -n 50 /var/log/nginx/error.log
sudo journalctl -u nginx --since "15 minutes ago"

Typical causes include:

  • The Node.js process stopped or is listening on another port.
  • Docker published the port on a different interface.
  • Nginx points to localhost but the service is in another container.
  • The application binds only to an unexpected address.
  • A firewall or security policy blocks the upstream.

Do not solve a 502 by exposing more ports before confirming the service topology.

Uploads, timeouts, and static files

Raise client_max_body_size only when the application needs larger uploads. A large value without application-level limits can increase resource risk.

For long-running requests, set a deliberate timeout rather than using an unlimited value:

location /api/ {
    proxy_read_timeout 120s;
    proxy_pass http://127.0.0.1:3000;
}

If Nginx serves static files directly, use an explicit directory and ownership policy. Never serve .env, private keys, backups, or the entire project root.

Safe deployment checklist

  1. Check the application locally or in staging.
  2. Verify the upstream port on the VPS.
  3. Edit the active site configuration with a backup.
  4. Run sudo nginx -t.
  5. Reload Nginx, then check the public health endpoint.
  6. Review access and error logs.
  7. Keep the previous application release available for rollback.

Frequently asked questions

Should Node.js listen on port 80?

Usually no. Let Nginx own ports 80 and 443 and keep Node.js on a private port.

Why does Nginx return 502 while the app works locally?

The host, port, address, container network, or permissions used by Nginx may differ from your shell test. Test the exact upstream address from the same server and read the Nginx error log.

Is a reload safer than a restart?

Yes. A successful reload keeps existing workers serving while Nginx loads the new configuration. Always run nginx -t first.

Conclusion

Nginx is a small, powerful boundary between the public internet and a Node.js application. Keep the upstream private, forward the original request context, configure HTTPS after DNS is ready, and verify every change with nginx -t, a health check, and the logs.

Continue reading