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 #

Without and with a front proxy
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-For and friends — the headers that carry the real client IP through.

What the front door buys #

CapabilityHow it shows up
TLS everywhereOne certificate config, all apps covered
Virtual hostingMany domains on one IP
Zero-downtime deploysProxy switches backends while old ones drain
Load balancingproxy_pass to several upstreams
Caching and rate limitsAt the edge, before apps do work
Consistent loggingOne 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 #

SymptomCause
App logs show 127.0.0.1 as every clientMissing X-Forwarded-For handling
Redirects to http:// in an HTTPS siteApp unaware of X-Forwarded-Proto
WebSocket upgrades 400Proxy missing Upgrade/Connection headers
Works from localhost, fails via domainProxy routing or DNS
Client IP checks (rate limits) misfireTrusting 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.