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