Reverse Proxy & Load Balancing
Your app — Node, Python, Go, whatever — listens on localhost:3000. Users
arrive on port 80 or 443. The bridge between those two facts is proxy_pass:
Nginx accepts the raw internet, then forwards clean requests to your app. This is the most
common Nginx job in existence.
💡 The Bottom Line
proxy_pass forwards a location's requests to a backend. Add the
proxy_set_header lines so the backend sees the real client instead of Nginx.
Need more than one backend? Declare an upstream group and pass to that —
load balancing included.
The standard proxy block
server {
listen 80;
server_name app.example.com;
location / {
proxy_pass http://localhost: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 four header lines matter because, from the backend's viewpoint, every
request now comes from Nginx (127.0.0.1). The headers restore the truth: which hostname was
requested (Host), the visitor's real IP (X-Real-IP /
X-Forwarded-For — logs and rate limiting break without it), and whether the
client used HTTPS (X-Forwarded-Proto — apps use this to build correct redirect
URLs). Copy this block whole; every omitted line is a subtle production bug later.
Load balancing: the upstream block
upstream api_backend {
server 10.0.0.11:3000;
server 10.0.0.12:3000;
server 10.0.0.13:3000 backup;
}
server {
listen 80;
location / {
proxy_pass http://api_backend;
}
}
Requests round-robin across the listed servers; a backend that stops answering is
temporarily dropped, and backup servers only receive traffic when the others are
down. Two useful variants: least_conn; (send to the least-busy backend) and
ip_hash; (pin each client IP to one backend, for apps with in-memory
sessions).
Mixing hats: static + proxy in one server
location / {
root /var/www/frontend;
try_files $uri /index.html;
}
location /api/ {
proxy_pass http://localhost:3000;
# + the proxy_set_header lines
}
The frontend served from disk at Nginx speed, /api/ proxied to the app —
the standard production layout for a web app, in eight lines.
🙋 There Are No Dumb Questions
Q: Why not just let users hit my Node app on port 3000 directly?
A: You'd be re-implementing Nginx badly inside your app: TLS termination, slow-client
buffering (a user on bad Wi-Fi ties up your app for the whole download without a proxy in
front), static file performance, one entry point for many services, and restarting your app
without dropping connections. App servers are good at running your code; Nginx is good at
everything between your code and the internet.
⚠️ Watch Out
Trailing slashes in proxy_pass change the forwarded path.
proxy_pass http://localhost:3000; (no path) forwards /api/users
as-is; proxy_pass http://localhost:3000/; (with slash) strips the matched
location prefix, forwarding /users. Both are useful; mixing them up produces
mysterious 404s that "work with curl on the backend." Lesson 6 revisits this — it's gotcha
royalty.
🔧 Try It Yourself
Proxy to a tiny backend using Docker Compose (both concepts from the Docker track):
services:
backend:
image: hashicorp/http-echo
command: ["-text=hello from the backend"]
nginx:
image: nginx:1.27
ports: ["8082:80"]
volumes:
- ./proxy.conf:/etc/nginx/conf.d/default.conf:ro
With proxy.conf containing a server block that does
proxy_pass http://backend:5678;. Then docker compose up -d and
visit http://localhost:8082 — Nginx in front, app behind, the production
pattern in miniature.
✅ Quick Check
Why does a proxied backend need the X-Forwarded-For header?
Next up: the missing piece between users and either hat — HTTPS: certificates, the 443 server block, and Let's Encrypt.