Nginx Reverse Proxy: How to Configure proxy_pass and Headers

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. When the upstream uses https and hosts several sites on the same IP address, also add proxy_ssl_server_name on; so Nginx sends the hostname through SNI during the TLS handshake.
The configuration above tells Nginx to forward every request that matches the /app location to the upstream at http://127.0.0.1:8080. If you are not sure which location Nginx picks when several blocks could match the same request, see our guide on Nginx location blocks
.
/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.
How proxy_pass Handles the URI
A single trailing slash in proxy_pass changes the path the upstream receives, and it is one of the most common reasons a backend answers with a 404 error after you put Nginx in front of it.
The table below shows what the upstream receives for a request to /app/users with different location and proxy_pass combinations:
| location | proxy_pass | Upstream receives |
|---|---|---|
/app/ | http://127.0.0.1:8080 | /app/users |
/app/ | http://127.0.0.1:8080/ | /users |
/app/ | http://127.0.0.1:8080/api/ | /api/users |
/app | http://127.0.0.1:8080/ | //users |
In the first row, proxy_pass has no URI, so Nginx forwards the path exactly as the client sent it. In the second and third rows, Nginx strips the matched /app/ prefix and puts the URI from proxy_pass in its place, either / or /api/.
The last row is the one that catches most people. The location /app has no trailing slash, but the proxy_pass URI does. Nginx replaces /app with / and appends the rest of the path, which also starts with a slash, so the upstream receives //users. Many applications do not route paths with a double slash and return a 404 error. To avoid this, keep the trailing slashes consistent and end both the location and the proxy_pass URI with /.
There are cases where Nginx cannot work out which part of the URI to replace. When the location is a regular expression or a named location (@name), proxy_pass must be specified without a URI. If you add one, the configuration test fails, as shown in the Troubleshooting
section below.
The rules change once proxy_pass contains a variable. If the directive includes a URI, Nginx sends that URI as is and does not append the rest of the request path. For example, with proxy_pass http://$backend/api/;, a request to /app/users reaches the upstream as /api/. If the variable holds a domain name, Nginx resolves it at request time, so you also need a resolver directive. Without it, the request fails with a 502 error and the error log shows no resolver defined to resolve. Names that match an upstream group do not need a resolver.
Setting Headers with proxy_set_header
By default, Nginx rewrites the Host header when it proxies a request and sets it to the upstream host ($proxy_host). Nginx versions before 1.29.7 also set Connection to close, while newer versions keep the upstream connection alive and do not send that header at all. 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. Nginx versions before 1.29.7 default to HTTP/1.0; version 1.29.7 and later 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. Test the configuration first, so a typo does not take the site down:
sudo nginx -t && sudo systemctl reload nginxSee the Nginx commands reference for other commands you can use to manage the service.
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).
Load Balancing with an upstream Block
When you run more than one instance of the application, list them in an upstream block in the http context and point proxy_pass to the group name instead of a single address:
upstream backend {
server 127.0.0.1:8081;
server 127.0.0.1:8082;
}
server {
listen 80;
server_name example.com;
location / {
proxy_pass http://backend;
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;
}
}With this configuration, Nginx sends requests to the two servers in turn, using a weighted round-robin method. If one of them stops responding, Nginx marks it as unavailable for a short time and passes the requests to the remaining server.
You can change how Nginx picks a server with the following options:
least_conn- Sends each request to the server with the fewest active connections. Add it on its own line inside theupstreamblock.ip_hash- Sends requests from the same client IP address to the same server, which helps applications that keep session data in memory.weight=N- Added to aserverline, sends proportionally more requests to that server. For example,server 127.0.0.1:8081 weight=3;receives three times as many requests as a server with the default weight of 1.backup- Added to aserverline, marks a server that receives requests only when the primary servers are unavailable. It cannot be combined withip_hash.
Starting with Nginx 1.29.7, connections to the upstream servers are kept alive and reused by default. On older versions, add keepalive 16; to the upstream block, together with proxy_http_version 1.1; and proxy_set_header Connection ""; in the location, so Nginx does not open a new connection for every request.
For an overview of other ways to spread traffic across servers, see our guide on Linux load balancing techniques .
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.5-fpm.sock;
}
}Adjust the socket path to match the PHP-FPM version installed on your system (for example php8.5-fpm.sock on Ubuntu 26.04 or php8.4-fpm.sock on Debian 13).
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 on;
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;
}
}The http2 on; directive enables HTTP/2 for the server block. On Nginx versions older than 1.25.1, such as the stock package on Ubuntu 24.04, this directive is not available; use listen 443 ssl http2; instead.
If you do not have a certificate yet, use Certbot to obtain a free Let’s Encrypt certificate and configure Nginx. When the proxy sits on an internal network and is never reached from the Internet, a self-signed certificate is enough.
Quick Reference
For a printable quick reference, see the Nginx cheatsheet .
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 |
proxy_read_timeout | 300s | Wait longer for slow upstream responses (default is 60s) |
proxy_ssl_server_name | on | Send the hostname through SNI to an https upstream |
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).
504 Gateway Timeout
The upstream accepted the connection but did not send a response within proxy_read_timeout, which defaults to 60 seconds. The error log shows upstream timed out (110: Connection timed out) while reading response header from upstream. If slow responses are expected, such as a long report or a large export, raise the timeout only in the location that serves them, for example proxy_read_timeout 300s;. Otherwise, check why the application takes so long to answer.
“proxy_pass” cannot have URI part in location given by regular expressionnginx -t reports this error when a regular expression or named location uses proxy_pass with a URI, such as proxy_pass http://127.0.0.1:8080/app/;. Remove the URI from proxy_pass. If you need to change the path, use a prefix location instead, or rewrite the URI with rewrite ... break; before the proxy_pass line.
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 1000+ Linux tutorials with 20+ years of experience turning complex Linux tasks into clear, reliable guides.
View author page