fiber optic network

Nginx Reverse Proxy for Node.js and Python on VPS

June 2026 · 6 min read

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:

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
Debug X-Forwarded-For: If your backend shows Nginx's IP (127.0.0.1) instead of the real client IP, verify that your app is reading the 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.