What is it? #
A reverse proxy sits between the internet and your application. Clients connect to it; it forwards requests to your application and returns the responses.
The difference from a forward proxy is direction. A forward proxy acts on behalf of clients reaching out; a reverse proxy acts on behalf of servers receiving requests.
It exists because application servers are not good at the public-facing job. Handling TLS, serving static files, buffering slow clients, compressing responses and enforcing limits are all things a dedicated proxy does better.
It also gives you one place to route from. Several applications, several paths, one public address.
Think of it like this #
A receptionist at a building entrance. Visitors do not wander to individual desks. The receptionist checks them in, handles deliveries, directs them to the right department, and holds people who arrive early.
The people at the desks get to do their actual work.
Simple example #
A Python application runs on port 8000 and is not designed to face the internet. Nginx listens on 443, handles TLS, serves static files directly, and forwards everything else to the application.
Code #
internet ──▶ Nginx (443) ──┬──▶ /static/* served directly from disk
├──▶ /api/* → application on 127.0.0.1:8000
└──▶ /admin/* → admin service on 127.0.0.1:8100
server {
listen 443 ssl http2;
server_name example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
client_max_body_size 10M; # reject huge uploads early
gzip on;
gzip_types text/css application/javascript application/json;
# Static files: never bother the application
location /static/ {
alias /var/www/app/static/;
expires 30d;
add_header Cache-Control "public, immutable";
access_log off;
}
# Everything else goes to the application
location / {
proxy_pass http://127.0.0.1:8000;
proxy_http_version 1.1;
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_connect_timeout 5s;
proxy_read_timeout 60s;
}
# WebSocket upgrade, when needed
location /ws/ {
proxy_pass http://127.0.0.1:8000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 3600s;
}
}
Jobs a reverse proxy takes off your application
TLS termination certificates in one place
static files served from disk at high speed
compression gzip or brotli without application code
request limits body size, rate limits, timeouts
slow client buffering the app is not tied up by a slow connection
routing several services behind one domain
health and failover with several upstreams, it is also a load balancer
How it works #
proxy_pass http://127.0.0.1:8000 forwards to the application, which listens only on localhost. That single decision means the application cannot be reached from the internet except through the proxy.
The X-Forwarded-* headers tell the application what the client actually asked for. Without X-Forwarded-Proto, an application behind TLS termination thinks every request is plain HTTP and may generate wrong redirect URLs.
The /static/ block serves files straight from disk with long cache headers. Nginx does this far faster than an application framework, and it removes a large share of requests entirely.
client_max_body_size rejects oversized uploads at the proxy, before they consume application resources.
Timeouts matter. proxy_read_timeout bounds how long the proxy waits for the application; too short cuts off slow endpoints, too long ties up connections during an incident.
The WebSocket block shows the one non-obvious case: upgrading a connection requires HTTP/1.1 plus the Upgrade and Connection headers, and a much longer read timeout because the connection stays open.
Slow client buffering is an underrated benefit. The proxy accepts a slow upload or trickles out a response while the application finishes quickly and moves on.
Real-world use #
Nginx, Caddy, HAProxy and Traefik fill this role, and cloud load balancers do the same job as a managed service. Almost every production deployment has one.
In containerised setups, an ingress controller is a reverse proxy that reads routing rules from configuration, sending traffic to the right service based on host and path.
Hosting several applications on one server is straightforward with this layout: each application on its own local port, each with its own server block or location.
The common failure mode is forgetting that the application now sees the proxy as the client. Rate limiting by IP, geolocation and audit logs all break until the forwarded headers are configured and the application is told to trust them.
Common mistakes #
- Exposing the application port publicly as well, bypassing the proxy entirely.
- Omitting X-Forwarded-Proto, which breaks HTTPS-aware redirects.
- Leaving the default body size limit and rejecting legitimate uploads with a confusing error.
- Trusting forwarded headers from untrusted sources, allowing IP spoofing.
- Forgetting WebSocket upgrade headers and long timeouts for real-time endpoints.
Practice #
Put Nginx in front of a local application. Serve a static folder directly, proxy everything else, set a 5 MB upload limit, and confirm with curl -H "X-Forwarded-For: 1.2.3.4" how your application reads the client IP. Then verify the application port is not reachable from another machine.