140

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?

pavel_karoukin
  • 1,531
  • 2
  • 10
  • 5
  • 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. – Andrew Domaszek May 23 '14 at 22:28
  • The proper thing to do is to fix the application. – Michael Hampton May 25 '14 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 '20 at 21:20
  • 2
    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 '20 at 07:45

7 Answers7

155

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;
  }
}
Jens Bradler
  • 6,133
  • 2
  • 16
  • 13
  • 9
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; seems better – sivann May 24 '14 at 09: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. :-) – Jens Bradler May 25 '14 at 11:37
  • @JensBradler You seem more expert than me so, could you tell me what you think of my solution? I want to do the same because I run two copies of my website from two accounts on my ISP: `site1.myisp.com` and `site2.myisp.com` and they only respond to their respective name. I, now, own my domain name and I'd like to use my ISP website to load balance my servers. Isn't that a good reason? Thank you very much ;) – ncenerar Aug 20 '14 at 15:45
  • 1
    @ncenerar You can do that but this will bring you to a single point of failure: the load balancer. If this is for load balancing (not redundancy) you can also use DNS based load balancing in combination with DNS failover. – Jens Bradler Sep 24 '14 at 18:28
  • 2
    This answer reflects [advice of the official blog](https://www.nginx.com/resources/admin-guide/reverse-proxy/). – Bernard Rosset Apr 04 '16 at 16:05
  • Also passing theese options can be saviour for your dev team because the application will continue to work during development. – Dimitrios Desyllas Jun 26 '19 at 12:40
  • 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 '20 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 09 '21 at 07:53
  • To complement Marc's comment: From the [docs](http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_set_header): "`$host`'s value equals the server name in the “Host” request header field or the primary server name if this field is not present." I.e. usually the `Host:` as defined by the client is passed on. – argonym Aug 25 '21 at 11:37
  • 1
    This doesn't work in 2022. `$host` is not set to server name(like facebook.com), but instead for me at least, its giving `localhost`. So this answer is deprecated. – theprogrammer Apr 10 '22 at 20:51
34

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.

dr.dimitru
  • 113
  • 5
ncenerar
  • 451
  • 4
  • 5
  • I originally used Lua approach, but now switched completely to HAProxy which allows to do just what I wanted with standard configuration. – pavel_karoukin Aug 20 '14 at 18:58
  • I wonder how this affects performance (adding an extra `server { .. proxy_pass ... }` layer) – KajMagnus Jun 07 '20 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). – Lucas Basquerotto Mar 02 '21 at 20:12
9

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.

GreenReaper
  • 351
  • 3
  • 8
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;
  }
}
dalore
  • 151
  • 1
  • 2
  • 2
    No. At the time "proxy_set_header" is evaluated the $upstream_addr is still null, it get's choosed later. – Honza Jul 27 '21 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;
        }
}
pavel_karoukin
  • 1,531
  • 2
  • 10
  • 5
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.

lyngvi
  • 121
  • 2
  • 1
    According to the docs https://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_set_header `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 '20 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/

Mazeryt
  • 133
  • 1
  • 7