How to set up a reverse proxy with Nginx: a practical guide
A reverse proxy sits in front of your web servers and forwards client requests to them. This guide shows you exactly how to set one up with Nginx — proxy_pass, headers, load balancing, TLS and common pitfalls.
On this page
- What a reverse proxy does
- Prerequisites
- Installing Nginx
- A minimal reverse proxy config
- Forwarding the right HTTP headers
- Load balancing with the upstream directive
- WebSocket and long-lived connections
- Adding TLS with a free certificate
- Checking and reloading the config
- Common pitfalls
- When you do not need a reverse proxy
- FAQ
- Is Nginx a web server or a reverse proxy?
- What is the difference between a reverse proxy and a load balancer?
- Do I need a certificate on both Nginx and my back-end app?
- How do I make my app see the real client IP?
- Does a reverse proxy slow my site down?
- Verdict
A reverse proxy is a server that sits in front of one or more web servers and forwards client requests to them, so clients never talk to your origin servers directly. Nginx is the most widely deployed web server and one of the most common tools for this job. This guide walks you through installing Nginx, writing a reverse-proxy config, and handling headers, load balancing and TLS.
What a reverse proxy does
On the Internet, a forward proxy sits in front of a group of clients and forwards their requests to servers on their behalf. A reverse proxy does the opposite: it sits in front of one or more origin servers, intercepts requests from clients at the edge, and forwards them to the right back-end. To clients, the reverse proxy is the only server they ever contact.
This setup is useful for several reasons:
| Job | What the reverse proxy does |
|---|---|
| Hide your back-end | Clients never see the origin server's address, open ports or identity |
| Load balancing | Distributes requests across many back-end instances |
| TLS termination | Handles HTTPS certificates in one place, at the edge |
| Caching | Serves repeated responses without hitting the back end |
| Compression | Gzip/Brotli-compresses responses to reduce transfer size |
| Security layer | Filters some attacks (DDoS, slow-loris, malformed requests) before they reach your app |
Nginx is an open-source web server released under the 2-clause BSD license, and it handles all of the above. Because it uses an event-driven architecture, it can handle more than 10,000 simultaneous connections with a low memory footprint, which is why it is so widely used as a reverse proxy and load balancer.
Prerequisites
Before you start you need:
- A Linux server (Ubuntu/Debian commands are shown below) running Nginx.
- At least one back-end application listening on a port (for example a Node.js or Python app on
http://127.0.0.1:3000). The back end does not need to be publicly reachable — the reverse proxy talks to it on the local network. - A domain name and, for TLS, a way to get a certificate (Let's Encrypt free certificates work well with Nginx).
Installing Nginx
On Debian or Ubuntu, install with:
sudo apt update
sudo apt install nginx
When it finishes, Nginx starts automatically and serves a default page on port 80. Check it is running with:
sudo systemctl status nginx
On most distributions the config lives in /etc/nginx/nginx.conf, with site configs in /etc/nginx/sites-available/ and symlinked into /etc/nginx/sites-enabled/. You can also put small config snippets in /etc/nginx/conf.d/.
A minimal reverse proxy config
The core directive that makes Nginx a reverse proxy is proxy_pass. It tells Nginx which back-end server to forward a request to. A minimal server block looks like this:
server {
listen 80;
server_name example.com;
location / {
proxy_pass http://127.0.0.1:3000;
}
}
With this config, a request to http://example.com/ is forwarded to http://127.0.0.1:3000/, where your Node.js or Python app runs. Clients only ever see example.com; they never learn that the app lives on port 3000.
proxy_pass can target any upstream: a local port, another server on your network, or a Unix socket for a service like the PHP-FPM application server.
Forwarding the right HTTP headers
By default Nginx forwards the request but the back-end app does not know the original client's IP or protocol, because to the back end every request looks like it comes from the proxy (usually 127.0.0.1). You fix this by forwarding the standard proxy headers:
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;
}
What each header does:
Host $host— tells the back end which hostname the client asked for. Without this, apps that route by domain name (virtual hosts) break.X-Real-IP $remote_addr— the client's real IP address.X-Forwarded-For— the full chain of client IPs the request passed through. Many apps read this to get the real client IP.X-Forwarded-Proto $scheme— whether the original request was HTTP or HTTPS. Apps use this to build correct absolute links and to know if the request arrived over TLS.
Note that these headers are not inherently trustworthy: a client can set a fake X-Forwarded-For. Only trust the values your own proxy adds. $remote_addr in Nginx is the IP Nginx actually saw, so X-Real-IP is usually the one to log and trust.
Load balancing with the upstream directive
To spread requests across several back-end instances, define an upstream block and point proxy_pass at it by name:
upstream app_servers {
server 127.0.0.1:3000;
server 127.0.0.1:3001;
}
server {
listen 80;
server_name example.com;
location / {
proxy_pass http://app_servers;
}
}
By default Nginx uses round-robin — each new request goes to the next server in the list. You can change the strategy:
least_conn— sends each request to the server with the fewest active connections; good for uneven request durations.ip_hash— always sends the same client IP to the same back-end; useful when an app keeps per-user state in memory.
upstream app_servers {
least_conn;
server 127.0.0.1:3000;
server 127.0.0.1:3001;
}
If one back-end goes down, Nginx notices and stops routing new requests to it (inactive servers are marked down), so a single failing instance does not take the whole site with it.
WebSocket and long-lived connections
Most modern apps use upgrade headers for WebSockets or streaming. You forward them like this:
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
proxy_http_version 1.1 is required because HTTP/1.1 is needed for the Upgrade and Connection headers to work correctly with a proxy. Many guides add this block to application routes that stream data (SSE, WebSockets, gRPC-Web).
Adding TLS with a free certificate
HTTPS is not optional for a production site. The easiest path is Let's Encrypt via certbot:
sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d example.com -d www.example.com
Certbot edits your Nginx config to add the certificate and redirects HTTP to HTTPS. The result is a server block that terminates TLS and forwards plain HTTP to your back end:
server {
listen 443 ssl;
server_name example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
One important detail for apps behind TLS: because the connection between Nginx and your back end is now plain HTTP, your app will see X-Forwarded-Proto: https. If your app needs to know the original protocol (to generate secure links or decide whether to set a Secure cookie), it must read X-Forwarded-Proto — and you must make sure the app trusts that header only from your own proxy.
Checking and reloading the config
Nginx config is strict: one syntax error and the service fails to start. Always test before reloading:
sudo nginx -t
If the test passes, reload Nginx without dropping connections:
sudo systemctl reload nginx
nginx -t validates the config; reload applies it gracefully. This is the safe loop for every config change.
Common pitfalls
- The proxy is a security surface. Since every public request hits Nginx, apply rate limits, keep it patched, and don't expose debug endpoints through it.
- Forgotten proxy headers. An app that sees
127.0.0.1as every client IP and generates HTTP links behind TLS is almost always missing theX-Forwarded-*headers above. - Trusting client-supplied headers. Letting arbitrary clients set
X-Forwarded-Forunchecked lets attackers spoof their IP in your logs. Only append headers from the proxy; don't blindly trust the whole chain. - One server block per site. Put each site in its own
serverblock with its ownserver_name; thelisten 80 default_serverblock catches requests for domains you did not configure. - Reload, not restart.
systemctl restart nginxdrops active connections;reloaddoes not.
When you do not need a reverse proxy
If you have a single static site and no rolling deployments, a plain web server without proxying is enough. Add a reverse proxy when you: run a dynamic app on a local port, run several apps on one machine (each gets its own server_name or path), need load balancing across instances, want to centralize TLS and caching, or want to hide your origin server. For many cloud setups, managed providers (Cloudflare Pages, load balancers, API gateways) already give you reverse-proxy behavior, so you only operate Nginx yourself when you are self-hosting.
FAQ
Is Nginx a web server or a reverse proxy?
Both. Nginx originally served static files, but it is equally capable as a reverse proxy, load balancer, HTTP cache and mail proxy. In practice the same proxy_pass configuration turns Nginx into a reverse proxy in front of one or more back-end servers.
What is the difference between a reverse proxy and a load balancer?
A load balancer distributes traffic across several back-end instances; a reverse proxy forwards traffic from clients to back-end servers and can add TLS, caching, compression and security. Because Nginx does both, the two roles often blur — in Nginx you define upstream blocks for load balancing, which is one feature inside the broader reverse-proxy setup.
Do I need a certificate on both Nginx and my back-end app?
No. With Nginx terminating TLS, the public connection is HTTPS while the connection from Nginx to your back-end is often plain HTTP on a local port. That is normal and the standard setup for a single reverse proxy. Your app must then read X-Forwarded-Proto to know the original request was HTTPS.
How do I make my app see the real client IP?
Forward the proxy headers from Nginx (X-Real-IP, X-Forwarded-For, X-Forwarded-Proto) as shown above, and configure your app to trust a single upstream — your Nginx instance. Never trust an unbounded list of X-Forwarded-For values from the Internet.
Does a reverse proxy slow my site down?
A well-configured reverse proxy adds only a few milliseconds per request and is usually a net win because it offloads TLS, compression and caching from your application servers. The proxy should be stateless and never store session data, so any instance can handle any request and a failed instance can be replaced without downtime.
Verdict
A reverse proxy is the standard way to expose dynamic applications safely and reliably. Nginx makes it straightforward: one server block, a proxy_pass to your back-end port, the four forwarding headers, and optionally an upstream block for load balancing. Start with the minimal config, always run nginx -t before reload, forward the headers your app needs, and terminate TLS once at the edge. Once it is in place, your origin servers stay private, certificates live in one place, and you can scale by adding more back-end instances behind the same proxy.
Sources
- Wikipedia — Nginx (en.wikipedia.org/wiki/Nginx
- CC BY-SA)
- MDN — Proxy servers and tunneling (developer.mozilla.org/en-US/docs/Web/HTTP/Proxy_servers_and_tunneling
- CC-BY-SA 2.5+)
- Cloudflare Learning — What is a reverse proxy (developers.cloudflare.com/learning/cdn/glossary/reverse-proxy/
- Cloudflare docs available for reuse)
Share this guide
Related guides
- What is load balancing? How load balancers work and key algorithms
Load balancing is the process of distributing incoming traffic across multiple servers so that no single server is overloaded. A load balancer sits in front of a pool of servers and decides where to send each request, trading total capacity for reliability and speed. This guide explains how load balancers work, the difference between Layer 4 and Layer 7, the main routing algorithms, and how to choose one.
- What is a proxy server and how does it work?
A plain explanation of proxy servers — forward vs reverse, what they do, the headers they add, and what they cannot protect.
- How DNS works: a plain explanation
What happens when you type a web address? A simple walkthrough of DNS — domain hierarchy, resolvers, caching and TTL.