152

I run several docker containers with hostnames:

web1.local web2.local web3.local

Routing to these done based on hostname by nginx. I have a proxy in front of this setup (on different machine connected to internet) where I define upstream as:

    upstream main {
      server web1.local:80;
      server web2.local:80;
      server web3.local:80;
    }

And actual virtual host description:

    server {
      listen 80;
      server_name example.com;
      location / {
        proxy_pass http://main;
      }
    }

Now, because containers receive hostname "main" instead of "web1.local", they do not respond properly to the request.

Question: how I can tell nginx to pass name of the upstream server instead of name of upstream group of servers in Host: header when proxying request?

4
  • 3
    I don't think you can. Why don't you set your backend servers to respond to main or example.com? It isn't as if the backend doesn't know who it is. The reverse is readily possible: proxy_set_header Host $host; will replace any Host variable coming back from the upstream with the hostname from the original request. May 23, 2014 at 22:28
  • The proper thing to do is to fix the application. May 25, 2014 at 12:59
  • 4
    @MichaelHampton That is not possible in some cases, eg if using proxy_ssl_server_name for TLS SNI, it needs the right server name.
    – Nate
    Jan 23, 2020 at 21:20
  • 3
    IMHO this is a bug in Nginx. The name "main" of upstream is just a local reference in the .conf file that does not need to reflect an actual hostname resolvable by DNS or known to the backend. Basically unless your backends know this reference or respond to Host: * you can't use Nginx's upstream directive.
    – Marc
    Feb 27, 2020 at 7:45

7 Answers 7

168

Actually you can do that via proxy_set_header.

For more details look here: http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_set_header or see an example use-case here: https://stackoverflow.com/questions/12847771/configure-nginx-with-proxy-pass

I have included the dynamic approach into your above posted configuration:

server {
  listen 80;
  server_name example.com;
  location / {
    proxy_pass       http://main;
    proxy_set_header Host            $host;
    proxy_set_header X-Forwarded-For $remote_addr;
  }
}

Here is an example with a static host name:

server {
  listen 80;
  server_name example.com;
  location / {
    proxy_pass       http://main;
    proxy_set_header Host            www.example.com;
    proxy_set_header X-Forwarded-For $remote_addr;
  }
}
11
  • 10
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; seems better
    – sivann
    May 24, 2014 at 9:00
  • 2
    @pavel: got it. Actually I did also some research and some tests. It seems that there is no straight approach to fulfill your requirement. So even a "bastardized" solution is a solution. I don't like to ask why you'd like to do this. I'm pretty sure you have your reasons. :-) May 25, 2014 at 11:37
  • 2
    This answer reflects advice of the official blog. Apr 4, 2016 at 16:05
  • 4
    This answer implies that $host variable gets set to the value of the server name from the upstream block server directive but that doesn't seem to match the documentation. If nginx chooses e.g. web1.local:80 as the destination, does $host get set to web1.local:80 or to example.com (the first value in server_name)? The latter might work but it's not what was originally asked.
    – kbolino
    Jan 27, 2020 at 20:25
  • 2
    This answer is incorrect - passing the incoming $host variable to your backend will only work if your NGINX is called on the SAME host as your backend responds to - an unlikely scenario.
    – Marc
    Jul 9, 2021 at 7:53
37

I had the same problem and I finally solved it by using two levels of proxy. Here is how you could do for your situation (I think):

server {
  listen      8001 default_server;
  server_name web1.example.com;
  location / {
    proxy_pass       http://web1.local:80;
    proxy_set_header Host web1.local:80;
  }
}

server {
  listen      8002 default_server;
  server_name web2.example.com;
  location / {
    proxy_pass       http://web2.local:80;
    proxy_set_header Host web2.local:80;
  }
}

server {
  listen      8003 default_server;
  server_name web3.example.com;
  location / {
    proxy_pass       http://web3.local:80;
    proxy_set_header Host web3.local:80;
  }
}

upstream main {
  server 127.0.0.1:8001;
  server 127.0.0.1:8002;
  server 127.0.0.1:8003;
}

server {
  listen      80;
  server_name example.com;
  location / {
    proxy_pass http://main;
  }
}

As you can see, the trick is to create a local server responding to a particular port that will proxy the server by rewriting the right Host for each servers. Then, you can use this local servers in your upstream and finally use that upstream in the real proxy.

4
  • 1
    I originally used Lua approach, but now switched completely to HAProxy which allows to do just what I wanted with standard configuration. Aug 20, 2014 at 18:58
  • I wonder how this affects performance (adding an extra server { .. proxy_pass ... } layer)
    – KajMagnus
    Jun 7, 2020 at 20:33
  • This is useful if you want to use a resolver with more than 1 server in upstream, because the free tier of nginx doesn't allow to resolve in the upstream (using a variable as the proxy name is not enough, because there's more than 1 endpoint, in the case in which nginx acts as a load balancer). Mar 2, 2021 at 20:12
  • 7 years after creating this answer it saved my hide. In my case each upstream also needs unique headers to each upstream so this worked perfectly.
    – Greg Biles
    Aug 10, 2023 at 16:43
10

While the goal seems logical, nginx isn't going to change the Host: header to match the upstream. Instead, it treats upstream domain names like a CNAME in DNS - as a way to get to an IP address.

The request headers (and body) are fixed before the upstream is selected. The upstream may change mid-request if it's a particular upstream is found to be non-responsive, but the request doesn't change.

5

We pass in the upstream addr as a separate header like this

server {
  listen 80;
  server_name example.com;
  location / {
    proxy_pass       http://main;
    proxy_set_header Host            $host;
    proxy_set_header X-Forwarded-For $remote_addr;
    add_header       X-Upstream      $upstream_addr;
  }
}

What if you tried?

server {
  listen 80;
  server_name example.com;
  location / {
    proxy_pass       http://main;
    proxy_set_header Host            $upstream_addr;
    proxy_set_header X-Forwarded-For $remote_addr;
    add_header       X-Host          $host;
  }
}
1
  • 2
    No. At the time "proxy_set_header" is evaluated the $upstream_addr is still null, it get's choosed later.
    – Honza
    Jul 27, 2021 at 20:08
3

So from reading all documentation for nginx (I could not really parse code for upstream module =( ) I came up with this bastardized solution. Unfortunately this solution does not keep track of failed hosts, but simply select random one and redirect request to it. So I have to setup some kind of monitoring to make sure all backends are running.

server {
        listen 80;
        server_name example.com;
        resolver 127.0.0.1;

        location / {
                set $upstream "";
                rewrite_by_lua '
                        local upstreams = {
                                "http://web1.dokku.localdomain",
                                "http://web2.dokku.localdomain",
                                "http://web3.dokku.localdomain",
                                "http://web4.dokku.localdomain"
                        }
                        ngx.var.upstream = upstreams[ math.random( #upstreams ) ] 
                ';
                proxy_pass $upstream;
        }
}
1
  • 2
    What happens if an upstream host is down with this method ?
    – Zulgrib
    Nov 5, 2020 at 0:46
2

Hmm. I have a similar setup, in which I've simply done

location / {
    ... 
    proxy_set_header X-Forwarded-Host $http_host;
    proxy_pass ...;
}

The use of $http_host (the HTTP Host header from the incoming request) here rather than $host (the server hostname configuration) causes the same Host header passed by the client to be passed up to the upstream, in my testing.

See also https://stackoverflow.com/questions/14352690/change-host-header-in-nginx-reverse-proxy.

1
  • 1
    According to the docs nginx.org/en/docs/http/… the $host variable - its value equals the server name in the “Host” request header field or the primary server name if this field is not present - so probably $host is better
    – keypress
    Feb 13, 2020 at 16:21
0

As other people already posted using script variable (like $upstream), you can set it however way you like, and that will fix the issue, without additional header hacking.

Proxy Pass handler threat script variables in a different way, if a value is not conditional (does not have $ in the name) is backed to the upstream on configuration phase and use later.

A simple way to omit this issue, and have the most advantages of (free version) upstream would be using something like Split_Clients:

split_clients $request_uri $my_upstream {
              33%          server1.domainX.com;
              33%          server2.domainX.com;
# Always use DOT at end entry if you wonder why, read the SC code.
              *            server3.domainX.com;  
}
location / {
    ... 
    proxy_pass http://$my_upstream;
}

The above example looks almost the same as upstream. There exists other modules do the mapping, i.e. chash_map_module, but as they are out of the tree you will need to build them by your own, which is not possible for some use-cases/

1
  • While simpler than lua, wouldn't solve the problem of one of the upstreams being down.
    – Honza
    Jul 27, 2021 at 20:10

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .