Nginx

Nginx Reverse Proxy Not Passing Headers Correctly

When headers your backend expects (like the real client IP, the original Host, or Authorization tokens) don't arrive correctly through an Nginx reverse proxy, it's because Nginx doesn't forward most headers automatically by default — you have to explicitly tell it which ones to pass through and how.

The Problem

Your application behind Nginx sees the wrong Host header, always sees Nginx's own IP instead of the real client IP, or loses custom headers like Authorization or X-API-Key entirely. Common symptoms in application logs:

Host header: localhost (expected: api.example.com)
Client IP: 172.18.0.3 (expected: the actual visitor's IP)
Authorization header: missing

Why It Happens

By default, Nginx's proxy_pass forwards the request but doesn't automatically preserve the original headers a well-behaved reverse proxy is expected to pass along. The most common causes are:

  • No proxy_set_header directives at all, so the backend receives Nginx's own default values instead of the client's original request data
  • The Host header isn't explicitly forwarded, so the backend sees Nginx's internal hostname or IP instead of the domain the visitor actually requested
  • Headers with underscores (like a custom X-Custom_Header) get silently dropped by Nginx unless explicitly allowed, since Nginx treats underscore-containing headers as potentially invalid by default
  • A misconfigured proxy_pass URL with a trailing slash mismatch subtly changes how the request path (and sometimes headers) get rewritten before reaching the backend

The Fix

Add the standard set of proxy headers to your location block — this is the baseline configuration nearly every reverse proxy setup needs:

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;
}

Each of these serves a specific purpose: Host ensures your backend sees the domain the visitor actually requested rather than Nginx's internal address, X-Real-IP and X-Forwarded-For preserve the visitor's real IP address instead of Nginx's own, and X-Forwarded-Proto tells the backend whether the original request was HTTP or HTTPS — important if your app makes decisions based on that (like redirecting to HTTPS, which can otherwise create a redirect loop).

If you need to forward a custom header with an underscore in the name, explicitly allow it, since Nginx drops these by default as a safety measure against header injection ambiguity:

underscores_in_headers on;

Place this directive inside the relevant server block. Without it, headers like X-Custom_Value are silently discarded before reaching your application, with no warning in the logs.

If your backend needs the Authorization header specifically and it's still not arriving, confirm nothing upstream (another proxy layer, a CDN, or a load balancer in front of Nginx) is stripping it before Nginx even receives the request — test directly against Nginx first to isolate where in the chain the header disappears:

curl -H "Authorization: Bearer test123" -v https://your-domain.com/api/endpoint

Still Not Working?

If headers are being sent correctly by Nginx but your application still doesn't see them as expected, check whether your application framework requires explicit configuration to trust proxy headers at all — many frameworks ignore X-Forwarded-For and similar headers by default unless you configure a list of trusted proxy IPs, precisely because trusting these headers from an untrusted source would be a security risk. If Nginx is your only entry point, adding your Nginx container's internal IP (or Docker network range) to your application's trusted proxies list is usually the missing piece.

It's also worth double-checking that no earlier location block or included config file is overriding your header directives further down the chain. Nginx applies configuration in a specific inheritance order, and a broader location / block setting headers doesn't automatically apply to a more specific block like location /api/ unless the headers are also set there, or unless include is used to share a common configuration snippet across blocks:

# snippets/proxy-headers.conf
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;
location /api/ {
    proxy_pass http://backend;
    include snippets/proxy-headers.conf;
}

Using a shared include file like this avoids the common mistake of correctly configuring headers in one location block while forgetting to repeat them in another, which can look like an intermittent or path-specific header issue when it's really just inconsistent configuration across blocks.