What is a reverse proxy?

A server that stands in front of your other servers, takes every request and decides who answers. What it does, when you need one, and a working Nginx example.

3–5 minutes
Network switch with many blue patch cables

A reverse proxy is a server that sits in front of one or more other servers and receives visitors’ requests on their behalf. The visitor talks only to the proxy. The proxy decides which server behind it should answer, passes the request along, and returns the reply as if it had produced it itself.

Forward proxy or reverse proxy?

Forward proxyReverse proxy
Works forThe people browsingThe website
Sits next toThe usersThe servers
HidesWhich user made the requestWhich server answered it
Typical useOffice web filters, privacy toolsHTTPS, load balancing, caching

What it is for

  • One public address, many apps. A blog, a shop and an API can each run on their own internal port while visitors only reach 80 and 443.
  • HTTPS in one place. The proxy holds the certificates; the apps behind it never deal with them.
  • Load balancing. The proxy shares traffic across several servers and skips any that fail.
  • Caching. It can serve stored copies of pages without waking the app. See caching.
  • A smaller attack surface. App servers are not reachable from the internet.

A working example with Nginx

A Node.js app listens on port 3000 of the same machine, and you want it at app.example.com:

server {
    listen 80;
    server_name app.example.com;

    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;
    }
}

proxy_pass makes it a reverse proxy. The proxy_set_header lines pass along details the app would otherwise lose: the requested domain, the visitor’s real IP, and whether they used HTTPS.

sudo nginx -t
sudo systemctl reload nginx

502 Bad Gateway on AlmaLinux, Rocky or RHEL? SELinux stops Nginx connecting to other ports by default. Allow it with sudo setsebool -P httpd_can_network_connect 1. Do not switch SELinux off. More causes in fixing 502 Bad Gateway.

Adding HTTPS and WebSockets

Once DNS points app.example.com at the server, Certbot adds HTTPS to this block like any other: sudo certbot --nginx -d app.example.com. The app itself keeps listening on plain HTTP on port 3000, which is safe because only Nginx can reach it.

Apps that use WebSockets, such as chat, live dashboards and many development servers, need two more lines in the location block, or connections drop after the first request:

proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";

Load balancing across servers

To share traffic between several app servers, list them in an upstream block and proxy to its name:

upstream app_servers {
    server 10.0.0.11:3000;
    server 10.0.0.12:3000;
    server 10.0.0.13:3000 backup;
}

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

    location / {
        proxy_pass http://app_servers;
        proxy_set_header Host $host;
    }
}

Nginx sends requests to each server in turn, stops using one that fails, and only uses the backup server when the others are down. Visitors who must stay on one server, for example because the app keeps sessions in memory, need ip_hash; in the upstream block, though shared session storage is the better fix.

Keep the app private

The point of the proxy is lost if the app is also reachable directly. Make the app listen on 127.0.0.1 rather than all addresses, and keep its port closed in the firewall. Check from outside:

curl -m 5 http://203.0.113.10:3000/

It should time out or be refused.

Common problems

SymptomCause
502 Bad GatewayThe app is not running, or is on a different port
504 Gateway TimeoutThe app is too slow; proxy_read_timeout ran out
App generates http:// links on an HTTPS siteIt ignores X-Forwarded-Proto; enable its “trust proxy” setting
Every visitor has the same IP in the app’s logsIt reads the connection address instead of X-Forwarded-For
ERR_TOO_MANY_REDIRECTSThe app redirects to HTTPS because it thinks requests are HTTP

Most frameworks have a single setting to trust the proxy’s headers, such as app.set('trust proxy', 1) in Express. Turn it on only when the app is behind a proxy you control.

Do you need one?

A single WordPress or PHP site already has what it needs. A reverse proxy earns its place when you run several apps on one server, run an app with its own built-in web server (Node.js, Python, Go, Java), or spread traffic over more than one machine.

Other common reverse proxies are Apache (mod_proxy), HAProxy for high-volume load balancing, Caddy, which sets up HTTPS automatically, and Traefik, which configures itself from Docker containers. CDNs such as Cloudflare are reverse proxies too, run at a global scale: see what a CDN is.

Something out of date? Software changes. If a step no longer works, tell us and we will check it and update the page.