🔀
Hosting

Nginx reverse proxy: putting Node.js and Python apps behind a domain

13.11.2026
← All articles

Apps built with Node.js, Python (Django, FastAPI, Flask) or similar stacks usually start their own built-in server on a port like 3000, 5000 or 8000. Typing http://server-ip:3000 into a browser works, but you cannot hand that address to customers. Visitors type example.uz and hit port 80 or 443. An Nginx reverse proxy bridges that gap: it accepts the public request, forwards it to the app behind it, and sends the response back.

What a reverse proxy actually does

A regular proxy sits on the user's side and goes out to the internet on their behalf. A reverse proxy sits on the server side, shields internal services from the outside world and answers for them. The browser only ever talks to Nginx and has no idea which port or how many instances the app runs on. That improves security, because the app port stays closed to the internet and listens only on 127.0.0.1. Nginx also terminates SSL, serves static files without touching the app, enables compression and can balance load across several instances while holding thousands of connections in very little memory.

Example: a delivery service API in Tashkent

Imagine a small Tashkent delivery service whose order API is written in Node.js and runs on a VPS on port 3000, while its admin panel is a Python FastAPI app on port 8000. The goal is for api.example.uz to reach Node.js and admin.example.uz to reach FastAPI, both over HTTPS. First, create A records for both subdomains pointing at the server IP, then configure Nginx:

server {
    listen 80;
    server_name api.example.uz;

    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;
    }
}

Enable the site and reload. Make running nginx -t before every reload a habit, since it catches syntax errors before they take the site down:

sudo ln -s /etc/nginx/sites-available/api.example.uz /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

Why the headers matter

Once Nginx forwards a request, every request looks to the app as if it came from 127.0.0.1. Without X-Real-IP and X-Forwarded-For the real client IP disappears from logs, and IP-based limits break. X-Forwarded-Proto tells the app the request arrived over HTTPS; without it Django or Express may think they are on plain HTTP and loop on redirects. The app must also trust these headers: app.set('trust proxy', 1) in Express, SECURE_PROXY_SSL_HEADER in Django, --proxy-headers in Uvicorn.

WebSocket and real-time features

Chat, live order tracking or notifications over WebSocket need extra lines, because WebSocket starts with an HTTP Upgrade and Nginx does not pass those headers by default:

proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 3600s;

By default Nginx drops the connection after 60 seconds without data from the backend, so long-lived sockets need a higher proxy_read_timeout or periodic pings from the app.

Python apps with Gunicorn and Uvicorn

Bind the app to the loopback address so the port is never exposed directly:

gunicorn myproject.wsgi:application --bind 127.0.0.1:8000 --workers 3
uvicorn main:app --host 127.0.0.1 --port 8000 --proxy-headers

For Django, serve the collectstatic folder straight from Nginx through a separate location /static/ block, leaving the app to handle only dynamic requests.

Adding SSL

Once HTTP works, Certbot can obtain a Let's Encrypt certificate and rewrite the Nginx config with a 443 block and an HTTP-to-HTTPS redirect:

sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d api.example.uz -d admin.example.uz

Certificates last 90 days and Certbot installs a renewal timer. Test it with sudo certbot renew --dry-run. Also keep the app itself alive: run it as a systemd service with Restart=always or under PM2, otherwise a crashed process turns into a 502 Bad Gateway.

Common mistakes

Where you can do this

Full control of Nginx requires root access, which means a VPS or dedicated server. sayt.uz VPS servers are hosted in a data centre in Uzbekistan, so latency for local users stays low. A plain PHP site needs none of this, because shared hosting already ships with Nginx and PHP configured. Whether a Node.js or Python app can run on shared hosting is best checked in the sayt.uz cabinet or with support.

Related articles

📤 Can't upload a large file: upload_max_filesize and post_max_size ⚡ Measuring Website Speed on Tas-IX: Commands and How to Read Results 🔌 Running WebSocket apps on hosting: shared hosting, VPS and Nginx setup 🐢 MySQL Slow Query Log: Finding the Query That Slows Your Site
🌐 Language
🇺🇿 O'zbek 🇺🇿 Ўзбек 🇷🇺 Русский 🇬🇧 English ✓