NGINX gotchas

NGINX is a wild beast of a program. It is one of the most straightforward but as a result one of the more intelligently complex proxy servers out there. And due to its rich feature set, it can be used for much more than just simple proxying. But that complexity and those features make it often unwieldly to use; combined with numerous bugs that have existed so long they’ve essentially become features, because changing them could break hundreds of thousands of websites were they to update NGINX. Here I’ve collected some of the “gotcha” moments I’ve experienced in my time using it. I will update this article should I come up with any more.

Inheritance

Martin Fjordvald made a nice article about this that I think explains it perfectly:

The default inheritance model is that directives inherit downwards only. […] When it comes to inheritance behaviour there are four types of configuration directives in nginx:

- Normal directive – One value per context, for example: “root” or “index”.
- Array directive – Multiple values per context, for example: “access_log” or “fastcgi_param”
- Action directive – Something which does not just configure, for example: “rewrite” or “fastcgi_pass”
- try_files directive.

Essentially, normal directives propagate downwards into new contexts; arrays do too but any changes will overwrite the previous array; action directives usually do not propagate and must be included in the child context if they should take effect. This is not obvious at first, and the syntax being similar to object-oriented languages might give a false impression that everything inherits in all cases, or that NGINX configuration is imperative (it isn’t).

If is Evil… with regular expressions

if directive matches create a new context. Because of this, action directives will not propagate downwards, like try_files. This is well known and documented, even by the developers themselves, leading to the idiom of If Is Evil. However, one often overlooked side-effect of this is positional and named captures from the location block being overwritten if regex is used in the if . The captures must be saved to other variables with the set directive, or the named capture syntax used ((?<varname>...)) if they are to be used inside an if with regex. I have not seen this documented elsewhere, but it makes sense and is likely not a bug.

root vs alias

Many people question the differences between the root and alias directives. They both set the document root, from which files are served. But while root’s functionality ends there, alias has a whole host of other things it does.

root

When using the root directive, the document root is set to the path specified in the directive. This path is exposed via the $document_root variable. System filenames in NGINX are often constructed relative to the document root, simply by concatenating the root and desired paths. The target file referenced in a request is also constructed in this manner, and is exposed via $request_filename. For example, with the following configuration and a request URI of /i/top.gif:

location /i/ {
    root /data/w3;
}

The document root is /data/w3, and the URI ($uri) is /i/top.gif. The $request_filename is hence /data/w3/i/top.gif. The default functionality in NGINX without an action directive is to serve the file present at that path. If the path points to a directory, NGINX will append a slash (/) to the URI automatically. If the ngx_http_index_module is enabled and the $uri ends with a slash, then index files will be tried under that directory (default index, index.html).

alias

When using alias, the document root is also set to the path specified in the directive, as with the root directive. However, this also marks the location directive block as using aliasing, which modifies the function of some directives and other code. Usually, before appending paths to the document root when constructing filenames, NGINX will attempt to remove the location URI prefix from the beginning of the paths. Since location matches based on prefixes in its most basic form, this results in the contents of the location directive to be effectively “replaced” with the alias. Let’s modify the example above to illustrate the process:

location /i/ {
    alias /data/w3/images/;
}

The $document_root is /data/w3/images/, and note that the $uri is still /i/top.gif as in the previous example. To alias the $request_filename, NGINX starts by looking at the URI, stripping the location block prefix /i/ from the beginning, and appending the result to the document root, which was set by the alias directive. As such, the $request_filename is now /data/w3/images/top.gif. This also explains why $document_root$uri doesn’t come out as expected when using aliases, which seems to be a common misunderstanding—the $uri variable isn’t changed, the directives and other code themselves strip the prefix before using the value.

Regular expressions in location and how it affects alias

When using regular expressions in location directives, NGINX doesn’t know where the location prefix ends, as it is not a simple URI that can be easily compared to the request URI. Technically, there are be ways around this that the developers could have employed, such as mandatory regex capture groups, but they opted to not go this route and instead rely on the configuration writer to account for this discrepancy. For this reason, NGINX developers recommend capture group references from the location directive to be used in the alias directive to correctly translate the URI into a full pathname, though this in itself has invited some confusion.1 2

As usual, the document root will be set to the value of the alias directive, and in this case that value ends up being a full pathname when following the above directions. This could introduce issues with other parts of the configuration that expect a higher level directory as the document root, especially FastCGI scripts. Additionally, since there is no functionality in NGINX to figure out where the location regex match ends in the URI, there is no way to remove the prefix from it either. Adding the whole untrimmed URI to the end of the document root when calculating $request_filename doesn’t make sense in the context of the alias directive, so it isn’t done. This has an obvious side-effect where $request_filename and $document_root are equal. Care needs to be taken that scripts and other components are using the right directories and files in this configuration.

So why would you want to use alias over other options when using regex in location?

  • alias points to a file or directory explicitly, while using root will see the entire URI appended to it to calculate the $request_filename, since as mentioned there is no functionality to trim any prefix;
  • alias and try_files can be combined to make the paths in try_files shorter (but note try_files with aliaslocation prefix replacement only works sometimes?);
  • alias is inherited by child blocks where try_files and other solutions are not;
  • On a micro-optimization level, performance with alias should be faster than with try_files, and should be very barely slower than root.

try_files with aliaslocation prefix replacement only works sometimes?

try_files handles replacing the location prefix itself when alias is used. However, it does not always replace the prefix, and this has resulted in continued confusion to a number of users. Looking at the example presented in this bug:

# bug: request to "/test/x" will try "/tmp/x" (good) and
# "/tmp//test/y" (bad?)
location /test/ {
    alias /tmp/;
    try_files $uri /test/y =404;
}

Logically, one would assume that the second path would resolve to /tmp/y as the prefix /test/ should match the prefix in the location directive. However, it turns out that try_files only replaces the prefix if the following criteria is met:

There has been heated debate over the source of this discrepancy, and whether or not it’s a bug; but looking at the code makes it plain to see that this is either a defect that needs to be fixed, or something that needs to be documented. The documentation specifically says:

The path to a file is constructed from the file parameter according to the root and alias directives.

Yet, this is not true if the alias directive is used and there is no variable or other script processing in the path.

I am fairly confident this could be fixed by simply moving the part that strips the prefix outside of the block for script processing:

However, fixing this issue could potentially break configurations, though I consider it unlikely. Since it has been 12 years as of this writing since the ticket was opened, it might as well be yet another layer of “yes this is a problem but it has existed for long enough that it is now expected behavior” that we have all come to know and love in NGINX.

See also

DigitalOcean Community: Understanding the Nginx Configuration File Structure and Configuration Contexts
Understanding Nginx Server and Location Block Selection Algorithms | DigitalOcean
Nginx location directive examples | DigitalOcean
Pitfalls and Common Mistakes | NGINX
Nginx Guts

Left-click: follow link, Right-click: select node, Scroll: zoom
x