Nginx Reverse Proxy Setup Guide for Node.js and Next.js Apps
On this page
Nginx Reverse Proxy Setup Guide for Node.js and Next.js Apps
Running a Node.js or Next.js application directly on port 80 or 443 is possible, but it is rarely the right choice for production. Nginx sitting in front of your application as a reverse proxy gives you SSL termination, load balancing, static file serving, request buffering, and a battle-tested layer of security that Node.js was never designed to handle on its own. This guide walks through a complete Nginx reverse proxy setup for both Node.js and Next.js applications, from installation to production-hardened configuration.
Why Use Nginx as a Reverse Proxy
Node.js is single-threaded by nature. While it handles asynchronous I/O exceptionally well, it struggles with CPU-bound tasks, serving large static files efficiently, and managing thousands of simultaneous slow connections. Nginx was built specifically for these problems. It uses an event-driven, non-blocking architecture that can handle tens of thousands of concurrent connections with minimal memory usage.
When Nginx sits in front of your Node.js or Next.js app, it absorbs slow clients, serves static assets from disk without touching your application process, terminates SSL so your app never deals with certificates, and provides a clean interface for load balancing across multiple application instances. The result is a faster, more stable, and more secure deployment.
Prerequisites
Before starting, ensure you have the following in place:
- A Linux server (Ubuntu 22.04 or later is used in this guide, but the concepts apply to any distribution)
- Node.js 18 or later installed
- Your Node.js or Next.js application ready to run
- Root or sudo access on the server
- A domain name pointed to your server's IP address (for SSL setup)
Installing Nginx
On Ubuntu or Debian-based systems, install Nginx with:
sudo apt update
sudo apt install nginx -y
Start the service and enable it to run on boot:
sudo systemctl start nginx
sudo systemctl enable nginx
Verify that Nginx is running by visiting your server's IP address in a browser. You should see the default Nginx welcome page.
Running Your Node.js App
Your Node.js application should listen on a local port, typically 3000 or any unprivileged port above 1024. A minimal Express server looks like this:
const express = require('express');
const app = express();
app.get('/', (req, res) => {
res.send('Hello from Node.js');
});
app.listen(3000, '127.0.0.1', () => {
console.log('Server running on http://127.0.0.1:3000');
});
Binding to 127.0.0.1 instead of 0.0.0.0 is important. This ensures the app only accepts connections from the local machine, forcing all external traffic through Nginx.
For Next.js, start your production build the same way:
next build
next start -p 3000 -H 127.0.0.1
Basic Nginx Reverse Proxy Configuration
Create a new site configuration file:
sudo nano /etc/nginx/sites-available/myapp
Add the following configuration:
server {
listen 80;
server_name yourdomain.com www.yourdomain.com;
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;
}
}
Enable the site and restart Nginx:
sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
Always run nginx -t before reloading. It validates your configuration and prevents downtime from syntax errors.
Understanding the Proxy Headers
Each header in the configuration serves a specific purpose:
- proxy_http_version 1.1 enables HTTP/1.1 to the backend, which is required for WebSocket connections and keepalive.
- Upgrade and Connection headers allow WebSocket connections to pass through the proxy. This is critical for Next.js hot module replacement during development and for any real-time features in production.
- Host preserves the original hostname so your application can generate correct URLs and handle virtual hosting.
- X-Real-IP passes the actual client IP address to your application, since without it your app only sees 127.0.0.1.
- X-Forwarded-For builds a chain of all proxies the request has passed through.
- X-Forwarded-Proto tells your application whether the original request was HTTP or HTTPS, which is essential for secure cookie handling and redirect logic.
Next.js-Specific Configuration
Next.js applications have additional requirements compared to plain Node.js servers. They serve static assets from the /_next/static/ path and benefit significantly from Nginx handling these files directly.
server {
listen 80;
server_name yourdomain.com;
# Serve Next.js static assets directly
location /_next/static/ {
alias /var/www/myapp/.next/static/;
expires 365d;
access_log off;
add_header Cache-Control "public, immutable";
}
# Serve public directory assets
location /static/ {
alias /var/www/myapp/public/static/;
expires 30d;
access_log off;
}
# Proxy everything else to Next.js
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;
}
}
By serving static files directly through Nginx, you remove a significant load from the Node.js process. Next.js hashes its static filenames, so setting a long cache expiry with the immutable directive is perfectly safe.
You should also configure Next.js to trust the proxy by setting the trustProxy option or, in newer versions, by adding "trustHost": true in your next.config.js.
Adding SSL with Let's Encrypt
Production applications must use HTTPS. Certbot makes this straightforward:
sudo apt install certbot python3-certbot-nginx -y
sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com
Certbot automatically modifies your Nginx configuration to include the SSL certificate paths and sets up a redirect from HTTP to HTTPS. It also installs a cron job to renew certificates before they expire.
After Certbot runs, your configuration will include an SSL server block with listen 443 ssl and the certificate paths filled in. Verify the renewal process works:
sudo certbot renew --dry-run
Production Hardening
A production-ready Nginx configuration should include several additional directives. Add these to the http block in /etc/nginx/nginx.conf or within your server block:
# Increase buffer sizes for large headers (common with JWTs)
proxy_buffer_size 128k;
proxy_buffers 4 256k;
proxy_busy_buffers_size 256k;
# Timeouts
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
# Gzip compression
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
# Rate limiting
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
Apply the rate limit to specific locations:
location /api/ {
limit_req zone=api burst=20 nodelay;
proxy_pass http://127.0.0.1:3000;
# ... other proxy headers
}
Load Balancing Multiple Instances
If you run multiple Node.js processes using PM2 or similar tools, Nginx can distribute traffic across them:
upstream nodejs_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 yourdomain.com;
location / {
proxy_pass http://nodejs_backend;
# ... proxy headers
}
}
The least_conn directive sends each new request to the backend with the fewest active connections. Other strategies include ip_hash for sticky sessions and the default round-robin.
Process Management with PM2
Your Node.js or Next.js app needs a process manager to stay alive after crashes and server reboots. PM2 is the standard choice:
npm install -g pm2
# For Node.js
pm2 start app.js --name myapp -i max
# For Next.js
pm2 start npm --name myapp -- start
# Save the process list and set up startup script
pm2 save
pm2 startup
The -i max flag runs one instance per CPU core, which pairs well with the Nginx upstream load balancing configuration shown above.
Practical Tips
Test configuration changes before reloading. Always run nginx -t before systemctl reload nginx. A bad configuration will take down all sites on the server.
Monitor your logs. Nginx access and error logs live in /var/log/nginx/. Tail the error log while debugging: tail -f /var/log/nginx/error.log.
Use separate log files per site. Add access_log /var/log/nginx/myapp.access.log; and error_log /var/log/nginx/myapp.error.log; inside your server block to keep logs organized.
Set appropriate client body size limits. If your app accepts file uploads, increase the default 1MB limit: client_max_body_size 50m;.
Disable server tokens. Add server_tokens off; to your http block to prevent Nginx from revealing its version number in response headers and error pages.
Keep Node.js bound to localhost. Never expose your application port to the internet. Nginx should be the only entry point.
Use health checks. Configure a simple health endpoint in your app (/health) and monitor it to detect failures quickly.
Troubleshooting Common Issues
502 Bad Gateway means Nginx cannot reach your Node.js application. Check that your app is running on the expected port and that proxy_pass points to the correct address. Run curl http://127.0.0.1:3000 from the server to verify.
504 Gateway Timeout indicates your application is taking too long to respond. Increase proxy_read_timeout or investigate slow endpoints in your app.
WebSocket connections failing usually means the Upgrade and Connection headers are missing from your configuration. Double-check that both are set correctly.
Mixed content warnings after adding SSL happen when your application generates HTTP URLs instead of HTTPS. Ensure the X-Forwarded-Proto header is being set and that your application respects it.
FAQ
Q: Can I use Nginx with Next.js standalone output mode?
A: Yes. When you set output: 'standalone' in next.config.js, the build produces a minimal server in .next/standalone/server.js. Point PM2 at this file and configure Nginx exactly as shown in this guide. The standalone server listens on port 3000 by default.
Q: Should I use Nginx or Caddy? A: Caddy is simpler to configure and handles SSL automatically with zero configuration. Nginx offers more control, better documentation, wider community support, and higher performance under extreme load. For most Node.js and Next.js deployments, both work well. Choose Nginx if you need fine-grained tuning or are already familiar with it.
Q: Do I need Nginx if I deploy to Vercel or a container platform? A: No. Platforms like Vercel, AWS App Runner, and Google Cloud Run provide their own reverse proxy and SSL termination. Nginx is for self-hosted or VPS deployments where you manage the infrastructure yourself.
Q: How do I handle multiple Node.js apps on one server?
A: Create separate Nginx server blocks for each domain, each proxying to a different local port. For example, app A runs on port 3000 and app B runs on port 3001, with separate configuration files in /etc/nginx/sites-available/.
Q: Is HTTP/2 supported with this setup?
A: Yes. After adding SSL with Certbot, enable HTTP/2 by changing listen 443 ssl; to listen 443 ssl http2;. Note that the proxy connection to your Node.js backend remains HTTP/1.1, which is fine because the performance benefit of HTTP/2 applies to the client-to-Nginx connection.
Q: How do I update Nginx without downtime?
A: Use sudo systemctl reload nginx instead of restart. The reload command gracefully starts new worker processes with the updated configuration while existing connections finish on the old workers. There is no downtime.
Q: What if my app uses Server-Sent Events (SSE)?
A: Add proxy_buffering off; to the location block serving SSE endpoints. Without this, Nginx buffers the response and SSE events will not stream to the client in real time.
Conclusion
Nginx as a reverse proxy is the industry-standard approach for deploying Node.js and Next.js applications in production. The combination gives you the developer experience and flexibility of Node.js with the performance, security, and stability of Nginx handling the network edge. Start with the basic configuration, add SSL with Certbot, then layer on static file serving, compression, and rate limiting as your traffic grows. The setup described in this guide will comfortably handle thousands of concurrent users on modest hardware.
The article is above (~1,800 words). I wasn't able to write it to a file due to permission restrictions. If you'd like it saved to disk, let me know and I can try an alternative approach.