Different proxy_pass upstream depending on client ip address in Nginx

Written by - 0 comments

Published on October 26th 2018 - Listed in Nginx Internet Linux

Sometimes there is a special situation when you need to show a different website to certain website users. Whether the situation is for clients coming from internal IP's, from specific countries (using GeoIP lookups) or bot user agents, ..., there are many use cases for such a need.This article will show the configuration in a Nginx web server, used here as a reverse proxy.

In this scenario certain client IP's needed to be (reverse) proxied to a different backend server (upstream) than the default server on which the "normal" web application is running.

The first idea how to implement the solution is (most likely) an if condition like this:

  location / {

    include /etc/nginx/proxy.conf;
    proxy_set_header X-Forwarded-Proto https;

    if ($remote_addr ~ "(|(") {
      proxy_pass https://different-upstream.example.com;

    proxy_pass http://default-upstream.example.com:8080;


The above config checks for the internal client IP addresses (saved in the global variable $remote_addr) and For these clients requests, the reverse proxy upstream is set to https://different-upstream.example.com. For all other clients, the default upstream (http://default-upstream.example.com:8080) is used.

Although this solution works, it is technically not advised for several reasons:

  • There is no else condition which clearly identifies an alternative action. Just leaving the second proxy_pass after the if condition becomes the "default".
  • If is evil. If you're an experienced Nginx administrator, you know about that already!

So if you care about a well working Nginx and a better solution, take a look at the http_geo_module and the http_map_module. This module's purpose is to create a map based on a condition with a defined target. Sounds complicated but it actually isn't. The following example will show you that.

First, above your server { } configuration, define the upstreams:

# Upstream definitions
upstream default {
  server default-upstream.example.com:8080;

upstream different {
  server different-upstream.example.com:443;

Note: The "different" upstream uses https. It is mandatory to define the port 443 in this case, otherwise default port 80 will be taken if no port is set.

Now comes the magic: The geo-map itself:

geo $remote_addr $backend {
  default http://default; https://different; https://different;

The map config explained:

  • geo: Tells Nginx to create a new "geo-map"...
  • $remote_addr: ... based on the client ip address ($remote_addr)...
  • $backend: ... and save the following value into the new variable $backend
  • default: This is a reserved keyword and specifies the default entry if nothing of the map is matched (in this scenario this means all client ip addresses which are not listed)

As you can see inside the map itself, default points to value "http://default;" which refers to the upstream called "default".
On the other hand, the entries with the two internal ip addresses point to value "https://different;", which, of course, is the upstream called "different".

Within the server { } configuration the map can be called, for example inside the location /:

server {

  location / {

    include /etc/nginx/proxy.conf;
    proxy_set_header X-Forwarded-Proto https;

    proxy_pass $backend;



Nginx now needs to access the $backend variable to determine the proxy_pass upstream. This calls the "map" entry from before which defines the wanted upstream server.

Note: It might also work to just define the full upstream URL inside the map, without having to define the upstreams first. But I didn't try that.

TL;DR: There's always a way around if, usually using a map. It's not that difficult to use, is easier to maintain than to add ip addresses to the if condition and most importantly ensures a well working Nginx config!

Update October 30th 2018: While fixed IP addresses worked with the "map" module, IP ranges (e.g. did not work. For this reason I switched to the "geo" map, which was made for working with IP addresses and ranges.

Add a comment

Show form to leave a comment

Comments (newest first)

No comments yet.