Nginx is a truly powerful, efficient, configurable web server. I’ve been using it for over 12 years. But for the longest time, I’ve been relying on simple configuration and basic rule sets to get by. While that’s no problem, I wanted to share what I have learned recently with map and geo usage in Nginx configuration.

I feel the Nginx documentation is loosely sufficient, but could do with some more examples. The map module explains only a couple of examples and some important information around wildcards and matching. The geo module is a little better, but documenting a couple of use cases here will help me, and maybe others wanting to delve into more complex Nginx configuration.

The map module

As explained in the documentation for the map module, it provides three directives:

The map directive is extremely flexible and powerful. It allows you to set the value of a variable based on another. Think of it like a case statement in your favourite programming language. After reading the documentation, I felt that explaining its using was better done with this approach:

map <InputValue> <OutputValue> {
  default <DefaultReturnValue>;

  <Match1> <Match1ReturnValue>;
  <Match2> <Match2ReturnValue>;
}

InputValue is the value being checked / matched against in this map. InputValue could be a string, or some other variable:

Plain text input value of "Hello":

map "Hello" $foo {
  default 0;
}

This is a naive map that doesn’t really have any functional use, however there are cases where you have your Nginx configuration generated by a system, where a static text value being input would make sense. More on this later.

Using a variable defined by Nginx ($host):

map $host $foo {
  default 0;

  "grahamweldon.com" 1;
  "slavitica.net" 2;
}

This is more interesting. Based on the $host variable (which is the server_name that is currently being accessed) a different value is output from this map: 1 if grahamweldon.com is being accessed, 2 if slavitica.net is being accessed, and 0 in every other case.

These OutputValues that are defined as the second variable in a map, are able to be referenced from anywhere within your Nginx http block.

For example, the following map defines a path to content, based on the $host variable:

map $host $site_content {
  default "public";

  "grahamweldon.com" "grahamweldon";
  "slavitica.net" "slavitica";
}

With this map in place, we could setup a server such as this:

http {
  [ ... ]

  server {
    listen 80 default_server;
    server_name _;
    location / {
      root /usr/local/websites/$site_content;
      try_files $uri index.html =404;
    }
  }
}

This server block defines itself as the default server, accepts any server name, listens on port 80 and serves a root directory based on the $host name via the map defined earlier. With a simple map in place we have a simple scalable, and easy to read virtual-hosting configuration!

Where I have found this to be extremely useful is in the case of conditional logging. You may have specific rules defining when to log information, or what information to log. On a recent project at work I use the map directive to enable/disable logging to a specific “alert” log file based on certain rules:

map $http_x_rakuten_alert $alert {
  default 0;

  "~*^ALERT$" 1;
}

This InputValue uses the HTTP Headers, specifically X-Rakuten-Alert (not the actual header we’re using, this is just an example) to determine if alerting should be enabled or disabled. Alerting in this case is done by logging to a log file ending with _alert.log which is picked up by filebeat, and shipped to other systems.

The location block defines a log file with custom conditions based on this map:

server {
  listen 80 default_server;
  server_name _;
  location / {
    access_log /var/log/web_alert.log custom if=$alert;
  }
}

That last piece on the access_log line: if=$alert causes the map to be looked up with the current request information, and since we’re returning either 0 or 1, this is used to determine if logging should be done for that request.

Map return values

An interesting point to note is that Nginx considers all map values returned to be “truthy” if they are non-zero or non-empty.

That is, 0 and "" are false values, if being passed to a conditional check like if=.

The geo module

The geo module is just as interesting. Its behaviour is the same in terms of input and output variables, but this time we can do complicated lookups based on IP address information. For example, we can perform access logging only if the request is NOT from the local network:

geo $remote_addr $log_request {
  default 1;
  
  10.0.0.0/8 0;
}

We could then reference this $log_request variable in a location block, like so:

location {
  root /some/path;
  access_log /var/log/access.log combined if=$log_request;
}

Any request coming from the 10.0.0.0/8 CIDR will not be logged.

Sometimes you don’t have full CIDR ranges you can specify, you might have to list distinct ranges. This can be done by starting the geo block with the ranges declaration:

geo $remote_addr $log_request {
  ranges;
  default 1;

  10.0.0.0-10.2.0.12 0;
  10.2.8.103-10.2.8.254 0;
}

Summary

These examples give you a glimpse of the possibilities with the map and geo commands, and hopefully will help you reduce complexity, improve readability of rule sets, and ideally: Replace all your if statements in your Nginx configuration!

Let me know how you’re configuring Nginx, and if this post helped you!