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
- Trailing slash in proxy_pass. With a slash inside
location /api/, the/api/prefix is stripped before forwarding. Match this to your app's routes. - App listening on 0.0.0.0. The port is then reachable around Nginx. Bind to localhost or firewall it.
- 413 on uploads. Nginx rejects bodies over 1 MB by default; set
client_max_body_size 20m;or similar. - 504 on slow endpoints. Raising timeouts helps, but moving heavy work to a background queue is the real fix.
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.