Setting up an Nginx Reverse Proxy

Putting Nginx in front of an application server is one of the most common patterns in modern web deployments. The application talks plain HTTP on a local port, and Nginx handles the public side: TLS termination, header rewriting, caching, compression, and load balancing across multiple backends.
A reverse proxy is a service that takes a client request, sends the request to one or more proxied servers, and forwards the response back to the client. Because of its performance and small memory footprint, Nginx is frequently used as a reverse proxy for HTTP servers such as Node.js , Python , and Java applications, and for non-HTTP backends like PHP-FPM and FastCGI.
Using Nginx as a reverse proxy gives you several benefits:
- Load balancing: Nginx can distribute client requests across multiple upstream servers, which improves performance, scalability, and availability.
- Caching: Nginx can cache upstream responses and serve them directly to clients, which reduces load on the backend and speeds up page delivery.
- TLS termination: Nginx can accept HTTPS connections, decrypt them, and talk to the upstream over plain HTTP. This keeps TLS configuration and certificates in one place.
- Compression: If the upstream does not send compressed responses, Nginx can compress them before sending them to the client.
- Rate limiting and access control: Nginx can limit the number of requests per client IP and restrict access based on client location or request headers such as
User-AgentandReferer.
This guide explains how to configure Nginx as a reverse proxy, covering the basic syntax, the proxy_set_header directives you will almost always want to set, and the most common issues you are likely to hit.
Prerequisites
We assume you already have Nginx installed on your Ubuntu
, Debian
, or another Linux server, and that you have root or sudo access to edit the Nginx configuration.
Using Nginx as a Reverse Proxy
To configure Nginx as a reverse proxy to an HTTP server, open the domain’s server block configuration file and declare a location that forwards to the upstream:
server {
listen 80;
server_name www.example.com example.com;
location /app {
proxy_pass http://127.0.0.1:8080;
}
}The proxy_pass directive sets the upstream URL. It accepts http or https as the protocol, a domain name or IP address, and an optional port and URI.
The configuration above tells Nginx to forward every request that matches the /app location to the upstream at http://127.0.0.1:8080.
/etc/nginx/sites-available. On Fedora, RHEL, and derivatives, they live in /etc/nginx/conf.d.To see how location and proxy_pass interact, consider the following example:
server {
listen 80;
server_name www.example.com example.com;
location /blog {
proxy_pass http://node1.com:8000/wordpress/;
}
}If a visitor accesses http://example.com/blog/my-post, Nginx proxies the request to http://node1.com:8000/wordpress/my-post.
When the proxy_pass address contains a URI (/wordpress/ in this case), the matched location prefix is replaced by that URI before the request is sent upstream. When the proxy_pass address has no URI, the original request URI is passed through unchanged.
Setting Headers with proxy_set_header
By default, Nginx rewrites two headers when it proxies a request. Host is set to the upstream host ($proxy_host) and Connection is set to close. Everything else the client sent is forwarded as-is, and empty headers are stripped.
This default is rarely what you want. In particular, the upstream sees $proxy_host as the Host header, not the original domain the client requested, and it sees 127.0.0.1 (Nginx itself) as the remote address. The fix is to set the headers your upstream actually needs with proxy_set_header.
For a normal HTTP reverse proxy, the following block is a good baseline:
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_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port $server_port;
}Each directive serves a specific purpose:
Host $host: forwards the original domain the client requested. Without this, the upstream sees the$proxy_hostvalue and cannot do name-based routing or generate correct absolute URLs.X-Real-IP $remote_addr: forwards the real client IP to the upstream. Without it, the upstream sees127.0.0.1.X-Forwarded-For $proxy_add_x_forwarded_for: appends the client IP to any existingX-Forwarded-Forchain, so the upstream can reconstruct the full path a request took through multiple proxies.X-Forwarded-Proto $scheme: tells the upstream whether the original request washttporhttps. Frameworks rely on this when they generate redirects or canonical URLs.X-Forwarded-Host $hostandX-Forwarded-Port $server_port: forward the original host and port the client requested, which is useful when the upstream listens on a different port than the public endpoint.
Websocket Reverse Proxy
Websockets need a few extra settings because they use the HTTP Upgrade mechanism. If your application serves a websocket endpoint, add a map in the http context and use the following location block for that endpoint:
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
# ... other directives
location /socket/ {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
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;
}
}Use these websocket-specific directives only for locations that actually handle websocket traffic:
proxy_http_version 1.1: use this when your upstream needs HTTP/1.1 features such as websocket upgrades or persistent connections. Older Nginx versions defaulted to HTTP/1.0; current releases default to HTTP/1.1.Upgrade $http_upgradeandConnection $connection_upgrade: required for websocket upgrades. ThemapsetsConnectiontoupgradeonly when the client requested an upgrade and falls back toclosefor normal requests.
If you have proxy_cache enabled, add proxy_cache_bypass $http_upgrade; so websocket upgrade requests are not served from cache.
To prevent a header from being passed to the upstream, set its value to an empty string. For example, this removes the Accept-Encoding header so the upstream always returns uncompressed responses:
proxy_set_header Accept-Encoding "";Reload Nginx after changing the configuration so the new settings take effect. See the Nginx commands reference for the exact commands.
proxy_set_header vs add_header
These two directives are often confused:
proxy_set_headersets a header on the request Nginx sends to the upstream.add_headersets a header on the response Nginx sends back to the client.
Use proxy_set_header when the upstream needs to see something (client IP, original host). Use add_header when the browser needs to see something (security headers, CORS, cache control).
Reverse Proxy to Non-HTTP Backends
Nginx can also proxy to backends that do not speak HTTP. Instead of proxy_pass, you use one of the protocol-specific directives:
fastcgi_pass: reverse proxy to a FastCGI server such as PHP-FPM.uwsgi_pass: reverse proxy to a uWSGI server.scgi_pass: reverse proxy to an SCGI server.memcached_pass: reverse proxy to a Memcached server.
The most common example is using Nginx as a reverse proxy to PHP-FPM :
server {
# ... other directives
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
}
}Adjust the socket path to match the PHP-FPM version installed on your system (for example php8.2-fpm.sock or php8.3-fpm.sock).
TLS Termination
Serving content over HTTPS is the default today. To terminate TLS at Nginx and proxy plain HTTP to the upstream, wrap the reverse proxy block inside a TLS-enabled server:
server {
listen 443 ssl http2;
server_name www.example.com 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-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}If you do not have a certificate yet, use Certbot to obtain a free Let’s Encrypt certificate on your Ubuntu , Debian , or CentOS server.
Quick Reference
Common proxy_set_header directives and the variables you typically pair them with:
| Directive | Value | Purpose |
|---|---|---|
Host | $host | Forward the domain the client requested, not $proxy_host |
X-Real-IP | $remote_addr | Send the client IP to the upstream |
X-Forwarded-For | $proxy_add_x_forwarded_for | Append the client IP to any existing chain |
X-Forwarded-Proto | $scheme | Tell the upstream whether the request was http or https |
X-Forwarded-Host | $host | Forward the original host the client requested |
X-Forwarded-Port | $server_port | Forward the original port the client connected to |
Upgrade | $http_upgrade | For websocket locations |
Connection | $connection_upgrade | For websocket locations |
Supporting directives outside proxy_set_header:
| Directive | Value | Purpose |
|---|---|---|
proxy_pass | URL of the upstream | Target of the reverse proxy |
proxy_http_version | 1.1 | Use when the upstream needs HTTP/1.1 features such as websockets |
proxy_cache_bypass | $http_upgrade | If proxy_cache is enabled, bypass cache for websocket upgrades |
Troubleshooting
502 Bad Gateway
Nginx cannot reach the upstream. Confirm the upstream service is listening on the address and port in proxy_pass (ss -tlnp or curl from the Nginx host), check that SELinux or a firewall is not blocking the connection, and look in /var/log/nginx/error.log for the exact reason (connection refused, timeout, or TLS handshake failure).
Client IP shows as 127.0.0.1 on the upstream
You are missing proxy_set_header X-Real-IP $remote_addr; or proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;. Add both and make sure the application reads from those headers. If the app is behind multiple proxies, you also need to configure it to trust the hop where your clients connect.
Websocket connections drop immediately
Websockets need HTTP/1.1 and the Upgrade/Connection header pair. Set proxy_http_version 1.1;, proxy_set_header Upgrade $http_upgrade; and proxy_set_header Connection $connection_upgrade; in the location that serves the websocket endpoint, and define the map in the http context.
Application generates http:// URLs even though the site is HTTPS
The app does not know the original request was HTTPS because Nginx is terminating TLS and forwarding over plain HTTP. Add proxy_set_header X-Forwarded-Proto $scheme; and configure the framework to trust it (for example, trust proxy in Express, SECURE_PROXY_SSL_HEADER in Django).
Virtual-host routing on the upstream breaks
The upstream sees $proxy_host as the Host header and cannot match it to any of its virtual hosts. Forward the original host with proxy_set_header Host $host;.
FAQ
How do I set a header in an Nginx reverse proxy?
Use proxy_set_header inside the location block that owns the proxy_pass. For example, proxy_set_header Host $host; forwards the original host header to the upstream. To remove a header from the proxied request, set its value to an empty string: proxy_set_header Accept-Encoding "";.
What is the difference between proxy_set_header and add_header?proxy_set_header modifies headers on the request Nginx sends to the upstream. add_header modifies headers on the response Nginx returns to the client. Use the first to communicate with the upstream, and the second to communicate with the browser (for example, to set Strict-Transport-Security or CORS headers).
Which headers should I forward to the upstream server?
At a minimum, forward Host, X-Real-IP, X-Forwarded-For, and X-Forwarded-Proto. For websockets, also set Upgrade and Connection $connection_upgrade and use proxy_http_version 1.1. Optional but useful: X-Forwarded-Host and X-Forwarded-Port when the public endpoint differs from the upstream address.
Why is the client IP showing as 127.0.0.1 on my backend?
Without explicit header configuration, the upstream only sees the address Nginx connects from, which is typically 127.0.0.1. Forward the real client IP with proxy_set_header X-Real-IP $remote_addr; and proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;, then configure the application to read those headers.
Conclusion
Nginx as a reverse proxy is mostly about getting two things right: pointing proxy_pass at the correct upstream, and forwarding the headers the upstream needs to behave as if the client had connected directly. Once those are in place, you can layer TLS termination, caching, load balancing, and rate limiting on top without touching the application. For the commands used to validate and reload the configuration, see Nginx commands you should know
.
Tags
Linuxize Weekly Newsletter
A quick weekly roundup of new tutorials, news, and tips.
About the authors

Dejan Panovski
Dejan Panovski is the founder of Linuxize, an RHCSA-certified Linux system administrator and DevOps engineer based in Skopje, Macedonia. Author of 800+ Linux tutorials with 20+ years of experience turning complex Linux tasks into clear, reliable guides.
View author page