Understanding Nginx Try Files

The nginx try_files directive is actually interesting! Not, like, amazingly interesting - but it has more depth than appears at first glance.

First, Nginx almost doesn't need try_files. Without it, Nginx could serve static files just fine:

server {
    listen 80;
    server_name _;

    root /var/www/html/public;
    index index.html index.html;
}

If we support PHP, we could have something like this:

server {
    listen 80;
    server_name _;

    root /var/www/html/public;
    index index.html index.html;

    location ~ \.php$ {
        # pass off to PHP-FPM via fastcgi
    }
}

That actually works for static files and the home page of our PHP application. Once we introduce a path into our URI (e.g. example.com/foo/bar), it breaks. This is where try_files comes in.

Adding try_files

The try_files directive is going to run through each option given in order to attempt to use its directives to find a file that exists on the server.

server {
    listen 80;
    server_name _;

    root /var/www/html/public;
    index index.html index.html index.php;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }
}

For a given URI, this will do the following:

The $uri option tells try_files to find the URI as given as a file on the disk drive relative to the root, which is /var/www/html/public in this case.

1️⃣ A URI of /css/app.css will search in /var/www/html/public/css/app.css.

2️⃣ A URI of /foo/bar will have 2 behaviors - one for if the directory exists, and one for if it does not.

First, the $uri/ option tells try_files to treat the URI as a directory and see if a directory exists. If the URI relates to an existing directory, Nginx needs to figure out what file to serve from that directory.

That's where the index directive comes into play. Since Nginx just knows a directory exist, we need index to tell Nginx which files to serve out of there (if they exist). You can have any files there. The first matched file "wins" and is served.

3️⃣ If the given URI matches neither an existing file nor directory, then try_files goes to the fallback URI - /index.php?$query_string;.

But other location blocks?

The location / block, and the try_files directive within it, actually work together with other location blocks! Here's slightly more complete configuration file:

server {
    listen 80;
    
    server_name _;
    root /var/www/html/foobar.com;

    index index.html index.htm index.php;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location ~ \.php$ {
        # pass off to PHP-FPM via fastcgi
    }

    location ~* \.(?:css(\.map)?|js(\.map)?|jpe?g|png|gif|ico)$ {
        expires    7d;
        access_log off;
        log_not_found off;
    }

    location ~ /\.(?!well-known).* {
        deny all;
    }
}

If the try_files directive resolves/finds a file that is a static asset (css, js, image), then the third location block actually handles the request. That means both location / {} and location ~* \.(<stuff>)$ {} blocks are relevant to such a request!

The same is true for when PHP files are used - the location ~ \.php$ {} block is used:

  1. When index resolves to index.php in a directory
  2. When the fallback /index.php?$requests_uri is used
  3. When a PHP file given by the URI exists as given

In these cases, the "matched" (used?) PHP file found by try_files is handled by the location ~ \.php$ {} block, which passes the request off to PHP-FPM. This is why a 404 error (whether for a static file or a non-existent application route) is generally returned from the PHP application. All real "can't find a file on the disk" cases are passed off to /index.php, and therefore the request is sent to the PHP application proxied via FastCGI in this configuration.