ESC

Start typing to search across invoices, services, domains, tickets, and more...

Search... Ctrl+K
Linux Server

Nginx Reverse Proxy Setup: Node/Python Apps, WebSocket, Real Client IP, HTTPS and 502 Fixes

6 steps 15 min read 3 views 0
On this page

Apps built with Node.js, Python (Flask, Django, FastAPI) or Go usually listen on a local port such as 127.0.0.1:3000 and should not face the internet directly. This guide shows how to set up an Nginx reverse proxy that forwards your domain to the backend app, handles WebSocket upgrades, passes the real client IP, adds free HTTPS with certbot and how to troubleshoot 502 Bad Gateway. Commands cover Debian/Ubuntu and Rocky Linux/AlmaLinux.

Step 1: Install Nginx and Check the Backend App

First confirm the app answers locally; if it does not, the proxy will only ever return 502. Keep the app bound to 127.0.0.1 so that only Nginx is public, and run it under systemd or pm2 so it restarts automatically after a crash.

# Debian / Ubuntu
apt update && apt install -y nginx
# Rocky Linux / AlmaLinux
dnf install -y nginx

systemctl enable --now nginx

# Make sure the backend app is listening locally (example: port 3000)
ss -lntp | grep 3000
curl -I http://127.0.0.1:3000

Step 2: Write the Nginx Reverse Proxy Config with WebSocket Support

Point your domain's DNS to the server IP, then create /etc/nginx/conf.d/app.conf. The map block lets both normal requests and WebSocket upgrades through; the X-Forwarded-* headers pass the client IP and original scheme to the app; a longer proxy_read_timeout keeps long-lived or slow requests from being cut off. Increase client_max_body_size if users upload large files.

# /etc/nginx/conf.d/app.conf  (included on Debian/Ubuntu and Rocky/AlmaLinux)
map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}

server {
    listen 80;
    listen [::]:80;
    server_name 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;

        # WebSocket support
        proxy_set_header Upgrade    $http_upgrade;
        proxy_set_header Connection $connection_upgrade;

        proxy_connect_timeout 10s;
        proxy_read_timeout    300s;
    }
}

Step 3: Test the Config, Reload Nginx and Open the Firewall

Always run nginx -t before reloading. On Rocky/AlmaLinux SELinux blocks Nginx from connecting to backend ports until you enable httpd_can_network_connect. Allow ports 80 and 443 in the firewall (see our Linux firewall tutorial for more rules).

nginx -t && systemctl reload nginx
curl -I -H "Host: app.example.com" http://127.0.0.1

# Rocky/AlmaLinux (SELinux): allow Nginx to connect to backend ports
setsebool -P httpd_can_network_connect 1

# Open HTTP/HTTPS in the firewall
ufw allow 'Nginx Full'                                   # Debian/Ubuntu with ufw
firewall-cmd --permanent --add-service=http --add-service=https
firewall-cmd --reload                                    # Rocky/AlmaLinux

Step 4: Pass the Real Client IP to Your App

Behind a reverse proxy every request appears to come from 127.0.0.1. Nginx already sends the real address in X-Real-IP and X-Forwarded-For; the framework just needs to trust the local proxy. If a CDN or load balancer sits in front of Nginx, restore the address in Nginx with the real_ip module and trust only the CDN's ranges so the header cannot be spoofed.

// Node.js / Express: trust the proxy on localhost
app.set('trust proxy', 'loopback');
// req.ip now returns the real client IP

# Python Flask / any WSGI app
from werkzeug.middleware.proxy_fix import ProxyFix
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1)

# Gunicorn
gunicorn --bind 127.0.0.1:8000 --forwarded-allow-ips="127.0.0.1" app:app

# Only if Nginx itself sits behind a CDN or load balancer (http or server block):
set_real_ip_from 203.0.113.0/24;     # the CDN / LB address ranges
real_ip_header X-Forwarded-For;
real_ip_recursive on;

Step 5: Enable HTTPS on the Reverse Proxy with Certbot

The certbot Nginx plugin obtains a Let's Encrypt certificate, adds a 443 listener to your server block and, with --redirect, sends HTTP visitors to HTTPS. The domain must already resolve to this server and port 80 must be reachable from the internet. Certificates last 90 days and renew automatically.

# Debian / Ubuntu
apt install -y certbot python3-certbot-nginx
# Rocky Linux / AlmaLinux (EPEL)
dnf install -y epel-release && dnf install -y certbot python3-certbot-nginx

certbot --nginx -d app.example.com --redirect -m [email protected] --agree-tos --no-eff-email

# Renewal runs from a systemd timer; test it:
certbot renew --dry-run
systemctl list-timers | grep -i certbot
If the app still generates http:// links after enabling HTTPS, it is not reading X-Forwarded-Proto. Configure proxy trust as shown in Step 4.

Step 6: Troubleshoot Nginx 502 Bad Gateway

A 502 means Nginx got no valid response from the backend. Start with the Nginx error log and match the message: connection refused means the app is down or on another port, permission denied usually means SELinux. Also note that on Node 17+ "localhost" may resolve to IPv6 ::1, so the app listens on [::1]:3000 while Nginx connects to 127.0.0.1.

tail -n 50 /var/log/nginx/error.log

# connect() failed (111: Connection refused)  -> app stopped or wrong port
# connect() ... (13: Permission denied)       -> SELinux: setsebool -P httpd_can_network_connect 1
# upstream prematurely closed connection      -> app crashed / restarted
# upstream timed out (110)                    -> app too slow: raise proxy_read_timeout or fix the app

systemctl status myapp
journalctl -u myapp -n 100 --no-pager
ss -lntp | grep -E ':3000|:8000'      # 127.0.0.1:3000 vs [::1]:3000 ?

FAQ

WebSocket fails with 400 or disconnects immediately?

Make sure proxy_http_version 1.1 and both Upgrade and Connection headers are set. If a CDN is in front, enable WebSocket support there too. Idle connections close after 60 seconds by default, so raise proxy_read_timeout or send heartbeats from the app.

Can one server reverse proxy several apps?

Yes. Create one server block per domain pointing to different ports, or route different location paths under one domain. If each site needs its own IP address, see our dedicated-IP-per-site Nginx tutorial.

Getting 504 Gateway Timeout instead?

504 means the backend answered too slowly. Optimise the slow request or raise proxy_read_timeout, and check the app log for stalls on the database or external APIs.

Still stuck after following these steps? Open a support ticket and the IMIDC 24/7 technical team will help. Include the server IP, OS version, the commands you ran and the full error output so we can pinpoint the issue faster.

Was this answer helpful?

Related Tutorials