Nginx Load Balancing: How to Dynamically Set Host Header Based on Upstream Server


3 views

When implementing Nginx as a load balancer, we often encounter backend servers that strictly validate the Host header. The problem becomes particularly acute when:

  • Backend servers are configured for strict virtual host matching
  • You're load balancing between servers with different domain names
  • The upstream servers don't share a common domain pattern

The intuitive approach of using $upstream_addr fails because:

proxy_set_header Host $upstream_addr;  # Returns IP:Port format (e.g., 192.168.1.10:80)

This won't match the expected domain name format that backend servers require.

The most robust solution uses Nginx's map module to create a dynamic Host header translation:

http {
    map $upstream_addr $target_host {
        default "";
        "~www\.asd\.com:80$" "www.asd.com";
        "~api\.example\.com:443$" "api.example.com";
        "~backend\d+\.internal:8080$" "app.company.com";
    }

    upstream myapp1 {
        server www.asd.com:80;
        server api.example.com:443;
        server backend1.internal:8080;
        server backend2.internal:8080;
    }

    server {
        listen 80;
        
        location / {
            proxy_set_header Host $target_host;
            proxy_set_header X-Forwarded-For $remote_addr;
            proxy_pass http://myapp1;
        }
    }
}

For simpler setups, you can define the host directly in the upstream block:

upstream myapp1 {
    server www.asd.com:80 host=www.asd.com;
    server api.example.com:443 host=api.example.com;
}

server {
    location / {
        proxy_set_header Host $host;
        proxy_pass http://myapp1;
    }
}

Watch out for these common pitfalls:

  • SSL termination points may need additional header configuration
  • WebSocket connections require consistent Host headers
  • Health checks might fail if not properly configured

Add these to your config to troubleshoot:

log_format upstream_debug '$remote_addr - $remote_user [$time_local] '
                         '"$request" $status $body_bytes_sent '
                         '"$http_referer" "$http_user_agent" '
                         'ups_addr: $upstream_addr ups_host: $target_host';

server {
    access_log /var/log/nginx/upstream.log upstream_debug;
}

When implementing Nginx as a load balancer, one common challenge arises when backend servers require specific host headers. Many web applications (especially legacy systems) are tightly coupled to their domain names and will reject requests with incorrect host headers.

The $upstream_addr variable contains the IP address and port of the selected upstream server, but it's not suitable for direct use as a host header. What we actually need is the domain name that corresponds to that upstream server.

Here's a robust solution using Nginx's map directive to create proper host header mappings:

http {
    upstream myapp1 {
        server www.asd.com:80;
        server www.example.net:80;
        server api.anotherdomain.com:8080;
    }

    map $upstream_addr $target_host {
        "~www.asd.com:80" "www.asd.com";
        "~www.example.net:80" "www.example.net";
        "~api.anotherdomain.com:8080" "api.anotherdomain.com";
        default $host; # fallback to original host
    }

    server {
        listen 80;

        location / {
            proxy_set_header Host $target_host;
            proxy_set_header X-Forwarded-For $remote_addr;
            proxy_pass http://myapp1;
        }
    }
}

For more dynamic environments where upstream servers might change frequently, consider this Lua-based approach:

http {
    lua_shared_dict host_mapping 10m;
    
    upstream myapp1 {
        server 192.168.1.10:80; # www.asd.com
        server 192.168.1.11:80; # www.example.net
    }

    init_by_lua_block {
        local host_map = {
            ["192.168.1.10:80"] = "www.asd.com",
            ["192.168.1.11:80"] = "www.example.net"
        }
        ngx.shared.host_mapping:set("map", cjson.encode(host_map))
    }

    server {
        listen 80;

        location / {
            access_by_lua_block {
                local host_map = cjson.decode(ngx.shared.host_mapping:get("map"))
                ngx.var.target_host = host_map[ngx.var.upstream_addr] or ngx.var.host
            }
            
            proxy_set_header Host $target_host;
            proxy_pass http://myapp1;
        }
    }
}
  • Always include a fallback (default $host) in your mapping
  • For HTTPS backends, ensure you also set proxy_ssl_server_name on
  • Monitor your error logs for cases where host header mismatches still occur
  • Consider using DNS resolution for more flexible configurations

If you're still seeing host header issues:

log_format upstream_debug '$remote_addr - $remote_user [$time_local] '
                          '"$request" $status $body_bytes_sent '
                          '"$http_referer" "$http_user_agent" '
                          'ups_addr:$upstream_addr ups_host:$target_host';

This custom log format will help you verify what host header is being sent to each upstream server.