Nearly every production service sits behind a reverse proxy — usually without anyone announcing it. The pattern explains a cluster of everyday mysteries: why the app sees 127.0.0.1 as the client, why HTTPS works without app changes, why one machine serves ten services.
The pattern #
client ──> app :8080 (app handles TLS, faces the internet alone)
client ──> proxy :443 ──> app :8080 (proxy: TLS, routing, logs)
──> app :3000 (apps stay private and simple)
The proxy accepts public connections; apps listen on localhost. One diagram, and the consequences unroll:
- TLS terminates once. Certificates, renewal, and protocol tuning live in the proxy — nginx or Caddy — not duplicated per app.
- Routing is central. Hostnames (
api.,app.) and paths (/api/) map to backends in one config; adding a service adds a block, not a port. - Apps see the proxy, not the client. Hence
X-Forwarded-Forand friends — the headers that carry the real client IP through.
What the front door buys #
| Capability | How it shows up |
|---|---|
| TLS everywhere | One certificate config, all apps covered |
| Virtual hosting | Many domains on one IP |
| Zero-downtime deploys | Proxy switches backends while old ones drain |
| Load balancing | proxy_pass to several upstreams |
| Caching and rate limits | At the edge, before apps do work |
| Consistent logging | One access log format for everything |
The deploy line matters most in practice: a deploy script that starts the new version on a new port and re-points the proxy achieves zero-downtime without the app knowing that was possible.
The classic symptoms, decoded #
| Symptom | Cause |
|---|---|
| App logs show 127.0.0.1 as every client | Missing X-Forwarded-For handling |
Redirects to http:// in an HTTPS site | App unaware of X-Forwarded-Proto |
| WebSocket upgrades 400 | Proxy missing Upgrade/Connection headers |
| Works from localhost, fails via domain | Proxy routing or DNS |
| Client IP checks (rate limits) misfire | Trusting headers blindly — see below |
The one security rule #
X-Forwarded-For is a header — clients can send it. Only the proxy's own appended value is trustworthy, so apps behind a proxy should read the header from the proxy's configuration (or the last hop), and anything internet-facing must not forward client-supplied X-Forwarded-* blindly. Getting this wrong is how IP allow-lists become decorative.
The test that reveals the whole pattern in one command: curl the proxy, then curl the backend port directly — same app, different path, different headers. The delta is everything the proxy does.
For implementations, start with nginx or Caddy; the TLS layer is certbot. The network series continues.
