Nginx Reverse Proxy for Node.js and Python on VPS
Most Node.js, Python Flask/Django, and Go applications run on high-numbered ports like 3000 or 8000, which means users must type the port in the domain name. Nginx Reverse Proxy solves this by accepting requests on port 80/443 and forwarding them to your app.
What is a Reverse Proxy?
A reverse proxy acts as an intermediary between the client and your backend application:
- Client sends a request to Nginx on port 443 (HTTPS)
- Nginx forwards the request to Node.js running on port 3000 of localhost
- Nginx receives the response from Node.js and sends it back to the client
The result is that the client only sees https://myapp.com and has no idea what port your backend runs on.
Basic Config: Node.js on Port 3000
Create the file /etc/nginx/sites-available/myapp:
server {
listen 80;
server_name myapp.com www.myapp.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name myapp.com www.myapp.com;
ssl_certificate /etc/letsencrypt/live/myapp.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/myapp.com/privkey.pem;
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;
}
}
ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/
nginx -t
systemctl reload nginx
Config for Python Gunicorn
Python apps (Flask/Django) typically run through Gunicorn on a Unix socket, which is faster than a TCP port:
location / {
proxy_pass http://unix:/run/gunicorn/myapp.sock;
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;
}
Running Multiple Apps on a Single Domain
You can route different paths to different backends:
server {
listen 443 ssl;
server_name myapp.com;
# Frontend React on port 3000
location / {
proxy_pass http://127.0.0.1:3000;
}
# API Node.js on port 4000
location /api/ {
proxy_pass http://127.0.0.1:4000/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
# Static files served by Nginx directly
location /static/ {
alias /var/www/myapp/static/;
expires 30d;
}
}
Timeout Settings for Long-Running Apps
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
If your app takes longer than 60 seconds to respond (such as video processing), you need to increase these values, otherwise Nginx will timeout before the app finishes.
Important Headers for IP Logging
Applications that need the real client IP must read from X-Real-IP or X-Forwarded-For headers, because Nginx (not the client) is the connection point with the backend.
Summary
Nginx Reverse Proxy is the standard pattern for deploying web applications on a VPS, consolidating SSL, compression, static file caching, and load balancing in one place — all without your application needing to know about any of these details.
Verify Reverse Proxy is Working Correctly
# Test config before reloading
nginx -t
# Reload config
systemctl reload nginx
# Verify request is being proxied correctly
curl -I https://myapp.com
# Look at X-Powered-By or Server header to confirm backend
# Check error log
tail -f /var/log/nginx/error.log
X-Forwarded-For or X-Real-IP header correctly.Common Issues & Troubleshooting
Error: "502 Bad Gateway"
The backend app is not running or the port/socket doesn't match:
# Check if app is running
ss -tlnp | grep :3000 # Node.js on port 3000
ps aux | grep node
# Test connect directly without going through Nginx
curl http://127.0.0.1:3000
# Check error log
tail -50 /var/log/nginx/error.log
Error: "504 Gateway Timeout"
Backend response is slower than the configured proxy timeout (default 60s):
# Increase timeout in nginx config
proxy_connect_timeout 120s;
proxy_send_timeout 120s;
proxy_read_timeout 120s;
nginx -t && systemctl reload nginx
Error: "413 Request Entity Too Large" When Uploading Files
# Add to server block or location
client_max_body_size 50M;
nginx -t && systemctl reload nginx
WebSocket Connection Drops Every 60 Seconds
Nginx requires special connection upgrade settings for WebSocket:
location /ws/ {
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_read_timeout 3600s; # No timeout
}
SSL Certificate Error After Certbot Renewal
# Test renewal
certbot renew --dry-run
# Reload nginx after renewal
systemctl reload nginx
# Check certificate expiry
echo | openssl s_client -connect myapp.com:443 2>/dev/null | openssl x509 -noout -dates
Frequently Asked Questions
Can multiple domains point to the same app?
Yes. Set server_name domain1.com www.domain1.com domain2.com in a single server block, or create multiple server blocks each proxying to the same backend.
How do I know if Nginx is proxying to the correct backend?
Check $upstream_addr in the access log, or temporarily add a debug header: add_header X-Debug-Upstream $upstream_addr; and view response headers with curl.
Can Nginx and Apache run on the same machine?
Yes. Have Nginx listen on port 80/443 and Apache listen on port 8080. Nginx receives requests and proxies them to Apache in the background, giving you the benefits of both.