Introduction

When Nginx proxies requests to an upstream server, it sets the Host header to the upstream server address by default (e.g., Host: 127.0.0.1:3000). This breaks virtual host routing on the backend, which expects the original client Host header (e.g., Host: example.com). The result is that the backend may serve the wrong site, generate incorrect redirect URLs, or reject the request entirely.

Symptoms

  • Backend application serves the default virtual host instead of the requested one
  • Redirect URLs point to the internal upstream address (e.g., http://127.0.0.1:3000/redirect)
  • API returns 404 because the backend routes requests based on the Host header
  • Backend framework (Django, Rails, Laravel) generates URLs with internal IPs instead of the domain
  • Multiple domains proxied to the same backend all resolve to the first virtual host

Common Causes

  • Nginx default behavior overwrites the Host header when using proxy_pass with a variable or IP
  • Missing proxy_set_header Host $host; directive in the location block
  • Backend application uses the Host header for routing, URL generation, or security checks
  • proxy_set_header is defined at the http level but overridden at the server or location level

Step-by-Step Fix

  1. 1.Set the Host header explicitly in the location block:
  2. 2.```nginx
  3. 3.server {
  4. 4.listen 80;
  5. 5.server_name example.com;

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

  1. 1.Preserve the original Host with port if your backend needs it:
  2. 2.```nginx
  3. 3.proxy_set_header Host $http_host;
  4. 4.`
  5. 5.This preserves the port number if present in the original request (e.g., example.com:8080).
  6. 6.Set trusted proxy headers for backend applications that need to know the client's original request. For Django in settings.py:
  7. 7.```python
  8. 8.SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
  9. 9.USE_X_FORWARDED_HOST = True
  10. 10.`
  11. 11.Verify the Host header is forwarded correctly by inspecting the upstream request:
  12. 12.```bash
  13. 13.curl -H "Host: example.com" http://127.0.0.1:3000/headers -v 2>&1 | grep Host
  14. 14.`
  15. 15.Or add a temporary logging format in Nginx:
  16. 16.```nginx
  17. 17.log_format upstream_debug '$upstream_addr - Host: $host - $request';
  18. 18.access_log /var/log/nginx/upstream_debug.log upstream_debug;
  19. 19.`
  20. 20.For multi-domain setups, use variables to dynamically set the Host header:
  21. 21.```nginx
  22. 22.upstream backend {
  23. 23.server 127.0.0.1:3000;
  24. 24.}

server { server_name ~^(www\.)?(?<domain>.+)$;

location / { proxy_pass http://backend; proxy_set_header Host $domain; } } ```

Prevention

  • Always include the standard proxy headers (Host, X-Real-IP, X-Forwarded-For, X-Forwarded-Proto) in every proxy location block
  • Extract common proxy settings into a separate file and include it:
  • ```nginx
  • # /etc/nginx/conf.d/proxy-params.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;
  • `
  • Then include /etc/nginx/conf.d/proxy-params.conf; in each location block.
  • Test multi-domain routing after any Nginx configuration change by requesting each domain and verifying the correct response
  • Use backend application logging to confirm the Host header arrives as expected