Nginx as a Reverse Proxy and Load Balancer¶
Basic reverse proxy¶
server {
listen 80;
server_name api.example.com;
location / {
proxy_pass http://127.0.0.1:8080;
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;
}
}
proxy_pass forwards the request to the backend named. Without the
proxy_set_header lines, the backend sees Nginx's own IP as the
client, and the original Host header is replaced with the upstream
address — breaking anything backend-side that logs client IPs, applies
per-IP rate limiting, or relies on the original hostname. These four
headers are close to mandatory for any real reverse-proxy setup, not
an optional hardening step.
Load balancing across multiple backends¶
upstream api_backend {
least_conn;
server 10.0.0.1:8080;
server 10.0.0.2:8080;
server 10.0.0.3:8080 weight=2;
}
server {
location / {
proxy_pass http://api_backend;
}
}
The upstream block names a pool of backend servers; proxy_pass
references the pool by name instead of one fixed address. This is a
direct, concrete instance of the
load balancing algorithms
already covered conceptually — least_conn here is the same
least-connections algorithm from that page, and the default (no
directive specified) is round robin. weight=2 sends roughly twice as
much traffic to that server, the weighted-round-robin variant from the
same page, useful when backend instances have different capacity
(a bigger machine in a mixed fleet).
Passive health checks¶
upstream api_backend {
server 10.0.0.1:8080 max_fails=3 fail_timeout=30s;
server 10.0.0.2:8080 max_fails=3 fail_timeout=30s;
}
Open-source Nginx does passive health checking only: after
max_fails consecutive failed proxy attempts to a server within
fail_timeout, Nginx marks it unavailable and stops routing to it for
fail_timeout before trying again. This is reactive — a backend has
to actually fail a real client's request before Nginx notices, unlike
an active health check that proactively probes backends on a timer
(available in Nginx Plus or via third-party modules, not in stock
open-source Nginx).
Common pitfall¶
Omitting proxy_set_header X-Forwarded-For (or not correctly
appending to an existing one via $proxy_add_x_forwarded_for rather
than overwriting it) silently breaks any backend logic that depends on
the real client IP — a rate limiter
keyed on client IP would instead key on Nginx's own IP for every
request, effectively rate-limiting the proxy as if it were a single
client, not each real client individually. This is easy to miss
entirely in testing (a direct connection to the backend during
development has no proxy in the way at all) and only surfaces once
traffic is actually flowing through the proxy in a real environment.