toolhq.io

All posts
7 min readby Jameel Haider

What is a reverse proxy, and why is one in front of nearly every app

A reverse proxy is a server that accepts requests on behalf of your application, then forwards them to whichever backend should handle them. Clients only ever talk to the proxy; they never learn the address of the machine actually running your code. Almost every production deployment has one, whether you configured nginx yourself or your platform runs one for you, because a handful of concerns are far better solved once at the edge than repeatedly inside every application.

Forward proxy or reverse proxy?

Both sit in the middle, but they serve opposite parties.

A forward proxy acts for the client. A corporate egress proxy or a VPN makes outbound requests on a user's behalf, so the destination server sees the proxy's address rather than the user's. The client knows it is using a proxy; the server usually does not.

A reverse proxy acts for the server. It sits in front of one or more backends and answers the public address. The client believes it is talking to the origin; only the operator knows there is a proxy at all.

Forward proxyReverse proxy
Configured byThe clientThe server operator
HidesThe client from the serverThe server from the client
Typical useEgress filtering, privacyTLS, routing, caching, load balancing
ExamplesSquid, corporate proxiesnginx, HAProxy, Cloudflare, Envoy

What does it actually do for you?

TLS termination. The proxy holds the certificate and private key, performs the TLS handshake, and speaks plain HTTP to the backend over a trusted network. Your application no longer needs to know anything about certificates, cipher suites, or renewals, and one certificate covers every service behind the proxy. You can confirm what a proxy presents with the SSL Checker.

Routing. One hostname, many services. /api goes to the API container, / goes to the frontend, /ws upgrades to a WebSocket backend. The client sees a single origin, which also sidesteps CORS entirely, since same origin requests never need it.

Load balancing and health checks. Multiple backend instances behind one address, with the proxy removing failing instances from rotation and retrying elsewhere. Round robin is the default; least connections suits long lived requests better.

Caching and compression. Static assets and cacheable responses can be served straight from the proxy according to your cache headers, never touching the application.

A security boundary. Rate limiting, request size limits, header normalisation, and blocking malformed requests all happen before your code runs. The backend can bind to localhost or a private network and be unreachable from the internet.

Protocol translation. The proxy can serve HTTP/2 and HTTP/3 to browsers while speaking plain HTTP/1.1 to a backend that has no idea those versions exist.

The headers that matter

Once a proxy is in the path, the backend loses its direct view of the client. Every request now arrives from the proxy's address, over plain HTTP, possibly on a different port than the user typed. Four headers restore that context:

X-Forwarded-For: 203.0.113.7        the original client IP
X-Forwarded-Proto: https            the scheme the client used
X-Forwarded-Host: app.example.com   the hostname the client requested
X-Real-IP: 203.0.113.7              client IP, single value convention

Three failure modes follow from getting this wrong, and all three are common:

  • Every user looks like the same IP. Rate limiting, geolocation, and audit logs all collapse onto the proxy's address because the application reads the socket address instead of X-Forwarded-For. Verify what your origin actually sees with the IP Lookup tool.
  • Infinite redirect loops. The application sees plain HTTP, redirects to HTTPS, the proxy forwards the new request as HTTP again, and the loop repeats. Fixing it means trusting X-Forwarded-Proto.
  • Spoofed client addresses. X-Forwarded-For is a client supplied header. If your application trusts it unconditionally, anyone can set it to any value and defeat IP based rules. Only trust it when the request came from a proxy you control, and configure the number of trusted hops explicitly rather than reading the first value in the list.

Most frameworks have a trusted proxy setting for exactly this. Set it deliberately; the safe default is to trust nothing.

A minimal nginx configuration

map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}

server {
    listen 443 ssl;
    http2 on;
    server_name app.example.com;

    ssl_certificate     /etc/ssl/certs/app.crt;
    ssl_certificate_key /etc/ssl/private/app.key;

    location / {
        proxy_pass http://127.0.0.1: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;

        # WebSocket upgrade support
        proxy_http_version 1.1;
        proxy_set_header Upgrade    $http_upgrade;
        proxy_set_header Connection $connection_upgrade;
    }
}

Two details carry most of the weight. proxy_set_header Host $host passes the hostname the client requested; without it nginx sends the upstream address instead, and any application that routes or generates links by hostname misbehaves. $proxy_add_x_forwarded_for appends rather than replaces, preserving the chain when several proxies are involved.

The TLS side of the file, protocol versions, ciphers, session settings, and HSTS, is worth generating rather than hand writing: the nginx config generator produces a current configuration, and secure nginx SSL config explained covers what each directive is doing.

When do you not need one?

If you deploy to a managed platform, a reverse proxy already exists in front of your code, terminating TLS, routing, and setting the forwarded headers. Running your own only makes sense when you need behaviour the platform does not offer. What you still owe your application in either case is the same: read the client address from the forwarded header, trust that header only from known proxies, and honour the forwarded scheme when generating URLs.