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.
Article by Phạm Minh Thiện
Front-end developer working directly with Next.js, NestJS, Docker, Nginx, and Ubuntu VPS deployments.
Reviewed on 7/27/2026.
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:3000or 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
localhostbut 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
- Check the application locally or in staging.
- Verify the upstream port on the VPS.
- Edit the active site configuration with a backup.
- Run
sudo nginx -t. - Reload Nginx, then check the public health endpoint.
- Review access and error logs.
- 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
Related articles
Nginx 502 Bad Gateway with Node.js: An Inside-Out Checklist
Find the cause of an Nginx 502 after deploying Next.js, NestJS, or Node.js with curl, ss, systemd, PM2, Docker Compose, and logs.
Read article →Install Free HTTPS with Certbot and Nginx on Ubuntu
Install a free Let's Encrypt certificate with Certbot and Nginx on Ubuntu, verify DNS and firewall access, renew automatically, and fix ACME challenge errors.
Read article →PostgreSQL Backups on a VPS with pg_dump and cron: 18 Steps with Restore Testing
Learn how to automate PostgreSQL backups on a VPS using pg_dump, cron, checksums, retention, offsite copies, and scheduled restore tests.
Read article →