Nginx Reverse Proxy and Load Balancing
Put Nginx in front of your app to reverse-proxy requests, balance load across instances, and forward WebSockets.
On your server or VPS, apps (Node.js, Python, Go, and so on) usually listen on ports like 3000 or 8080. Exposing those ports straight to the internet is rarely ideal: you get no HTTPS, no unified logging, and no way to spread traffic across instances. The common pattern is to have Nginx listen on 80/443 and reverse-proxy requests to the backend.
Install Nginx
Ubuntu / Debian:
sudo apt update
sudo apt install -y nginx
sudo systemctl enable --now nginx
Site configs live in /etc/nginx/sites-available/; symlink them into /etc/nginx/sites-enabled/ to activate them.
Basic reverse proxy
Forward requests to an app listening on port 3000:
server {
listen 80;
server_name example.com;
location / {
proxy_pass http://127.0.0.1:3000;
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;
}
}
Those proxysetheader lines matter:
- Host: passes the original hostname through, so virtual hosts and redirects resolve correctly.
- X-Real-IP: lets the backend see the real client IP instead of Nginx's.
- X-Forwarded-For: appends to the forwarding chain; $proxyaddxforwardedfor preserves any existing value.
- X-Forwarded-Proto: tells the backend whether the original scheme was http or https.
Always test before reloading, so a typo never takes the site down:
sudo nginx -t
sudo systemctl reload nginx
Load balancing with upstream
When you run several instances of an app (different ports, or different machines), group them in an upstream block and point proxypass at the group name:
upstream backend {
# least_conn; # optional: favor the instance with the fewest connections
server 127.0.0.1:3000;
server 127.0.0.1:3001;
server 10.0.0.12:3000 weight=2; # heavier weight, gets more traffic
server 10.0.0.13:3000 backup; # used only when the others are down
}
server {
listen 80;
server_name example.com;
location / {
proxy_pass http://backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
Common strategies:
- round-robin (default): hand requests out in turn, tuned by weight.
- leastconn: send each new request to the instance with the fewest active connections; great when request times vary widely.
- iphash: hash on the client IP so a given client always lands on the same instance (session stickiness).
Nginx probes backends passively: a server that fails repeatedly is dropped for a while, then retried.
Forwarding WebSockets
WebSockets rely on the HTTP Upgrade handshake, which isn't passed through by default — you have to add it explicitly:
location /ws/ {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_read_timeout 3600s; # keep long-lived connections from timing out
}
The essentials: set proxyhttpversion 1.1, pass the Upgrade and Connection headers through, and raise proxyreadtimeout so idle-but-open sockets aren't cut off early.
Summary
- A reverse proxy makes Nginx the single entry point; proxypass hands traffic to the backend port.
- Host / X-Real-IP / X-Forwarded-For / X-Forwarded-Proto give the backend the real client details.
- upstream groups multiple instances and spreads load via round-robin, leastconn, or iphash.
- WebSockets need the Upgrade/Connection headers forwarded and a longer read timeout.
- Run nginx -t before every reload to change config without downtime.