Setting up an Nginx Reverse Proxy

By 

Updated on

9 min read

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-Agent and Referer.

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:

nginx
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.

Info
On Ubuntu and Debian-based distributions, server block files live in /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:

nginx
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:

nginx
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_host value 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 sees 127.0.0.1.
  • X-Forwarded-For $proxy_add_x_forwarded_for: appends the client IP to any existing X-Forwarded-For chain, 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 was http or https. Frameworks rely on this when they generate redirects or canonical URLs.
  • X-Forwarded-Host $host and X-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:

nginx
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_upgrade and Connection $connection_upgrade: required for websocket upgrades. The map sets Connection to upgrade only when the client requested an upgrade and falls back to close for 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:

nginx
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_header sets a header on the request Nginx sends to the upstream.
  • add_header sets 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 :

nginx
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:

nginx
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:

DirectiveValuePurpose
Host$hostForward the domain the client requested, not $proxy_host
X-Real-IP$remote_addrSend the client IP to the upstream
X-Forwarded-For$proxy_add_x_forwarded_forAppend the client IP to any existing chain
X-Forwarded-Proto$schemeTell the upstream whether the request was http or https
X-Forwarded-Host$hostForward the original host the client requested
X-Forwarded-Port$server_portForward the original port the client connected to
Upgrade$http_upgradeFor websocket locations
Connection$connection_upgradeFor websocket locations

Supporting directives outside proxy_set_header:

DirectiveValuePurpose
proxy_passURL of the upstreamTarget of the reverse proxy
proxy_http_version1.1Use when the upstream needs HTTP/1.1 features such as websockets
proxy_cache_bypass$http_upgradeIf 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

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