LFCE 56 ๐ง Reverse Proxy โ Concepts
A reverse proxy sits in front of one or more backend servers and forwards client requests to them. The client sees only the proxy; the backend servers are hidden behind it. The proxy terminates the client connection, selects a backend, forwards the request, receives the response, and returns it to the client. The backend sees the proxy as the client, not the original caller, unless the proxy adds forwarding headers that preserve the original information. This architecture is the foundation of modern web deployments: it provides load balancing, TLS termination, caching, compression, and a single point of access control in front of an application that may be running on many servers.
The previous chapters covered the web servers themselvesโNginx, Apache, and Caddyโand each of them can act as a reverse proxy. This chapter covers the concepts that are common to all three: the request flow, the headers that preserve the client’s identity, the load-balancing algorithms, the health checks that remove failed backends from rotation, and the TLS termination that moves the encryption burden from the application to the proxy.
Key point: A reverse proxy is transparent to the client but not to the backend. The backend receives the connection from the proxy’s IP address, not the client’s, unless the proxy adds the X-Forwarded-For, X-Forwarded-Proto, and X-Forwarded-Host headers. The application must be configured to trust those headers from the proxy, and the proxy must be configured to set them, or the application logs the wrong client IP and generates redirects with the wrong scheme.
Why reverse proxies exist
The TLS-termination problem.
An application server that handles its own TLS must manage certificates, renewal, and cipher configuration for every instance. A reverse proxy terminates TLS once, at the edge, and forwards plain HTTP to the backends over a private network. The backends no longer need certificates, and the renewal is centralized at the proxy. The proxy can also enforce a minimum TLS version and a cipher policy that the application does not need to implement .
The load-balancing problem.
A single application server has a capacity limit. When the limit is reached, the solution is to run multiple instances behind a proxy that distributes requests among them. The proxy can use round-robin, least-connections, IP-hash, or weighted algorithms, and it can remove a backend from rotation when it fails a health check. The client sees one endpoint; the proxy handles the distribution and the failover .
The single-entry problem.
Multiple servicesโan API, a web frontend, a WebSocket endpointโcan be exposed through one proxy on one hostname. The proxy routes by path (/api/ to the API, /ws/ to the WebSocket, / to the frontend), so the client does not need to know which backend serves which path. This simplifies the client configuration and centralizes the routing rules in one place.
The header-preservation problem.
The backend needs to know the original client’s IP address, the original protocol (HTTP or HTTPS), and the original hostname. The proxy adds the X-Forwarded-For, X-Forwarded-Proto, and X-Forwarded-Host headers to the forwarded request. The application reads these headers when it constructs URLs or logs requests. Without them, the application sees the proxy’s IP and the internal protocol, which produces incorrect logs and redirect loops .
a. The request flow and forwarding headers
A client sends a request to the proxy. The proxy accepts the connection, terminates TLS if configured, selects a backend, and opens a new connection to the backend. The backend’s response is returned through the proxy to the client.
# Nginx reverse proxy configuration
server {
listen 443 ssl;
server_name example.com;
ssl_certificate /etc/nginx/ssl/example.com.crt;
ssl_certificate_key /etc/nginx/ssl/example.com.key;
location / {
proxy_pass http://127.0.0.1:3000;
# Preserve the client's information
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_set_header X-Forwarded-Host $host;
}
}
The proxy_pass directive specifies the backend address. The proxy_set_header directives add the forwarding headers. $host is the hostname from the client’s request, $remote_addr is the client’s IP address, and $scheme is http or https depending on the listener . The $proxy_add_x_forwarded_for variable appends the client’s IP to the existing X-Forwarded-For header, which preserves the chain when multiple proxies are involved.
The Host header is particularly important. By default, Nginx sends the backend’s hostname in the Host header, not the client’s. Many applications use the Host header to construct absolute URLs, and a mismatch produces redirects to the wrong domain. The proxy_set_header Host $host; directive preserves the client’s hostname . The X-Forwarded-Proto header tells the application that the client’s connection was HTTPS, even though the proxy-to-backend connection is HTTP. Without it, the application may generate an HTTP redirect that loops through the proxy.
b. Load-balancing algorithms
A proxy that fronts multiple backends must choose which backend receives each request. The algorithm determines the distribution and the failover behavior.
# Nginx upstream with least-connections
upstream backend {
least_conn;
server 10.0.0.1:3000 weight=3;
server 10.0.0.2:3000;
server 10.0.0.3:3000 backup;
}
server {
location / {
proxy_pass http://backend;
}
}
The upstream block defines the pool. The least_conn directive selects the backend with the fewest active connections, which is appropriate when requests have variable duration. The weight parameter gives a backend a larger share of the requests; a weight of 3 means it receives three times as many requests as a backend with the default weight of 1. The backup parameter marks a backend as a standby that receives traffic only when the primary backends are unavailable .
The round-robin algorithm is the default. It distributes requests evenly in sequence, which is appropriate when the backends have similar capacity and the requests have similar duration. The IP-hash algorithm maps the client’s IP address to a backend, which provides session persistence when the application does not share sessions between instances. The random algorithm selects a backend at random, optionally with a weight, which avoids the herding effect that round-robin can produce under certain conditions .
# IP-hash for session persistence
upstream backend {
ip_hash;
server 10.0.0.1:3000;
server 10.0.0.2:3000;
}
The ip_hash directive ensures that requests from the same client IP address consistently reach the same backend. This is a workaround for applications that store session state locally; the better solution is to store sessions in a shared store, but IP-hash is a valid option when the application cannot be changed .
c. Health checks and failover
A backend that has failed must be removed from rotation. The proxy detects failures through passive health checksโobserving the responses to real requestsโor active health checks that send probes to a dedicated endpoint.
# Passive health checks
upstream backend {
server 10.0.0.1:3000 max_fails=3 fail_timeout=30s;
server 10.0.0.2:3000 max_fails=3 fail_timeout=30s;
}
The max_fails parameter is the number of failed requests within the fail_timeout window that cause the backend to be marked as unavailable. A backend that returns a connection error or a timeout counts as a failure; a 5xx response counts only if proxy_next_upstream includes it. The backend is retried after the fail_timeout expires . This is a passive check: the proxy does not send probes, it observes the traffic.
Active health checks are available in Nginx Plus, the commercial edition, but not in the open-source edition. The open-source equivalent is to use a load balancer that supports active checks, or to rely on passive checks and fast failure detection. Caddy and Apache have their own health-check mechanisms, and the configuration differs, but the concept is the same: a backend that fails is removed from the pool, and a backend that recovers is returned to the pool.
Complete Example Session
# ============================================
# PART 1: START A TEST BACKEND
# ============================================
# A simple HTTP server on port 3000
python3 -m http.server 3000 --directory /var/www/backend &
# The backend listens on 127.0.0.1:3000
# ============================================
# PART 2: CONFIGURE THE REVERSE PROXY
# ============================================
sudo tee /etc/nginx/conf.d/proxy.conf > /dev/null <<'EOF'
upstream backend {
least_conn;
server 127.0.0.1:3000 max_fails=3 fail_timeout=30s;
}
server {
listen 80;
server_name proxy.local;
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;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
EOF
# ============================================
# PART 3: VALIDATE AND RELOAD
# ============================================
sudo nginx -t
sudo systemctl reload nginx
# ============================================
# PART 4: TEST THE PROXY
# ============================================
curl -H "Host: proxy.local" http://localhost
# Output: the content from the backend
# ============================================
# PART 5: CHECK THE FORWARDING HEADERS
# ============================================
# On the backend, log the headers
# The X-Forwarded-For header contains the client's IP
# The X-Forwarded-Proto header contains "http"
# ============================================
# PART 6: STOP THE BACKEND AND TEST FAILOVER
# ============================================
kill %1 # Stop the Python server
curl -I -H "Host: proxy.local" http://localhost
# Output: 502 Bad Gateway
# The proxy has no backend to forward to
# ============================================
# PART 7: ADD A SECOND BACKEND
# ============================================
python3 -m http.server 3001 --directory /var/www/backend2 &
# Update the upstream block to include 127.0.0.1:3001
# Reload nginx
# The proxy now distributes requests between the two backends
# ============================================
# PART 8: VERIFY LOAD BALANCING
# ============================================
for i in {1..10}; do curl -s -H "Host: proxy.local" http://localhost; done
# The responses alternate between the two backends
# ============================================
# PART 9: CHECK THE ACCESS LOG
# ============================================
sudo tail -5 /var/log/nginx/access.log
# Output shows the proxy's requests to the backend
# ============================================
# PART 10: CLEAN UP
# ============================================
kill %1 %2
sudo rm /etc/nginx/conf.d/proxy.conf
sudo systemctl reload nginx
The ten parts demonstrate the complete reverse proxy setup: a test backend, the proxy configuration, validation, testing, header inspection, failover testing, adding a second backend, load balancing verification, log inspection, and cleanup. The critical verification is Part 5: the backend receives the forwarding headers, and the client’s IP is preserved in X-Forwarded-For.
Quick Reference
Forwarding Headers
| Header | Purpose | Example |
|---|---|---|
Host | Original hostname | Host: example.com |
X-Real-IP | Client’s IP address | X-Real-IP: 203.0.113.5 |
X-Forwarded-For | Client IP chain | X-Forwarded-For: 203.0.113.5 |
X-Forwarded-Proto | Original protocol | X-Forwarded-Proto: https |
X-Forwarded-Host | Original hostname | X-Forwarded-Host: example.com |
Load-Balancing Algorithms
| Algorithm | Nginx Directive | Use Case |
|---|---|---|
| Round-robin | Default | Equal backends, similar requests |
| Least-connections | least_conn | Variable request duration |
| IP-hash | ip_hash | Session persistence |
| Weighted | weight=N | Unequal backend capacity |
| Random | random | Avoid herding |
Health-Check Parameters
| Parameter | Meaning |
|---|---|
max_fails=N | Failures before marking unavailable |
fail_timeout=Ns | Window for failures and retry interval |
backup | Standby backend |
down | Administratively disabled |
Best Practices
โ Do This:
# Preserve the client's Host header
proxy_set_header Host $host; # โ
# Add the forwarding headers
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # โ
proxy_set_header X-Forwarded-Proto $scheme; # โ
# Configure health checks
server 10.0.0.1:3000 max_fails=3 fail_timeout=30s; # โ
# Use least_conn for variable request durations
upstream backend { least_conn; ... } # โ
# Use weight for unequal capacity
server 10.0.0.1:3000 weight=3; # โ
# Terminate TLS at the proxy
listen 443 ssl; # โ
โ Don’t Do This:
# Forward without the Host header
proxy_pass http://backend; # Backend sees its own hostname # โ
# Omit X-Forwarded-Proto
# The application generates HTTP redirects that loop # โ
# Use round-robin for long-lived connections
# One backend receives most of the connections # โ
# Disable health checks
# Failed backends stay in rotation # โ
# Terminate TLS at the backend and the proxy
# Double encryption, double certificate management # โ
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Redirect loop | X-Forwarded-Proto not set | Add the header; configure the app to trust it |
| Wrong client IP in logs | X-Forwarded-For not set | Add the header |
| Wrong hostname in redirects | Host header not preserved | proxy_set_header Host $host; |
| 502 Bad Gateway | Backend down or wrong address | curl the backend directly |
| Uneven load distribution | Round-robin with variable requests | Use least_conn |
| Session loss | No session persistence | Use ip_hash or a shared session store |
Real-World Examples
1. Basic reverse proxy
location / { proxy_pass http://127.0.0.1:3000; }
2. Forwarding headers
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
3. Least-connections upstream
upstream backend { least_conn; server 10.0.0.1:3000; server 10.0.0.2:3000; }
4. Weighted backend
server 10.0.0.1:3000 weight=3;
5. Backup backend
server 10.0.0.3:3000 backup;
6. Health-check parameters
server 10.0.0.1:3000 max_fails=3 fail_timeout=30s;
7. IP-hash persistence
upstream backend { ip_hash; server 10.0.0.1:3000; }
8. TLS termination
listen 443 ssl; ssl_certificate /path/cert.pem; ssl_certificate_key /path/key.pem;
9. Test the proxy
curl -H "Host: proxy.local" http://localhost
10. Check the backend headers
# On the backend, inspect X-Forwarded-For and X-Forwarded-Proto
Visual
Reverse Proxy Request Flow
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ THE CLIENT SEES THE PROXY; THE BACKEND SEES THE PROXY โ
โ โ
โ Client โ
โ โโโโโโโโโโโ โ
โ โ Browser โ โ
โ โโโโโโฌโโโโโ โ
โ โ HTTPS โ
โ โผ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ Reverse Proxy โ โ
โ โ โโโ terminates TLS โ โ
โ โ โโโ selects backend โ โ
โ โ โโโ adds forwarding headers โ โ
โ โ โโโ forwards request โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ HTTP (internal) โ
โ โผ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ Backend โ โ
โ โ โโโ sees the proxy's IP as the client โ โ
โ โ โโโ reads X-Forwarded-For for the real client IP โ โ
โ โ โโโ reads X-Forwarded-Proto for the original scheme โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Load Balancing
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ THE PROXY DISTRIBUTES REQUESTS ACROSS BACKENDS โ
โ โ
โ Client requests โ
โ โ โ
โ โผ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ Proxy (upstream block) โ โ
โ โ โ โ
โ โ round-robin: 1 โ 2 โ 3 โ 1 โ 2 โ 3 โ โ
โ โ least_conn: backend with fewest active connections โ โ
โ โ ip_hash: same client โ same backend โ โ
โ โ weight: backend with weight 3 gets 3x requests โ โ
โ โโโโโโโโโฌโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโ โ
โ โ โ โ โ
โ โผ โผ โผ โ
โ โโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโ โ
โ โ Backend 1 โ โ Backend 2 โ โ Backend 3 โ โ
โ โ :3000 โ โ :3000 โ โ :3000 โ โ
โ โโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Health Check and Failover
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ FAILED BACKENDS ARE REMOVED FROM ROTATION โ
โ โ
โ Backend 1: healthy โ receives traffic โ
โ Backend 2: fails 3 requests within 30s โ marked unavailable โ
โ Backend 3: healthy โ receives traffic โ
โ โ
โ After 30s (fail_timeout): โ
โ Backend 2 is retried with a single request โ
โ โ โ
โ โโโ Success โ returned to rotation โ
โ โ โ
โ โโโ Failure โ marked unavailable for another 30s โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Summary
| Item | Value |
|---|---|
| Role | Forwards client requests to backend servers |
| TLS termination | At the proxy, not the backend |
| Client IP preservation | X-Forwarded-For, X-Real-IP |
| Protocol preservation | X-Forwarded-Proto |
| Host preservation | Host, X-Forwarded-Host |
| Default algorithm | Round-robin |
| Least-connections | least_conn |
| Session persistence | ip_hash |
| Weighted distribution | weight=N |
| Health checks | max_fails, fail_timeout |
| Standby backend | backup |
Key takeaways:
- The proxy terminates the client connection and opens a new one to the backend. The client sees the proxy; the backend sees the proxy’s IP unless the forwarding headers are set .
- The forwarding headers preserve the client’s information.
X-Forwarded-Forcarries the client IP,X-Forwarded-Protocarries the original scheme, andHostcarries the original hostname. The application must trust these headers from the proxy . - The load-balancing algorithm determines the distribution. Round-robin is the default, least-connections is better for variable request durations, IP-hash provides session persistence, and weight gives a backend a larger share .
- Health checks remove failed backends from rotation. Passive checks observe real requests;
max_failsandfail_timeoutcontrol the threshold and the retry interval. A backend that fails is marked unavailable and retried after the timeout . - TLS is terminated at the proxy. The backend receives plain HTTP over a private network. The certificate management is centralized at the proxy, and the backend does not need a certificate .
- The
Hostheader must be preserved. Without it, the backend sees its own hostname, and any absolute URL it constructs points to the wrong domain.proxy_set_header Host $host;is the standard fix .
Remember: A reverse proxy is the single entry point that hides the backend topology from the client. It terminates TLS, selects a backend, adds the forwarding headers, and returns the response. The load-balancing algorithm and the health checks determine which backend receives each request and how failures are handled. The forwarding headers preserve the client’s identity so that the application logs the right IP and generates the right URLs. The proxy is transparent to the client but not to the backend, and the headers are the mechanism that closes the gap.
Stop using slow, ad-bloated tool sites! ๐คฎ
๐ Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
โ
Finance (Mortgage, Interest, Inflation)
โ
Tech (Base64, JSON, Dev Suite, IP)
โ
Health (BMI, BMR, TDEE)
โ
Productivity (Timer, Workspace, QR)
โก๏ธ Fast & Private
๐ No data leaves your device
๐ 100% Free
๐ Use it now: https://tools.kandz.me
๐ Bookmark itโyouโll need it later!