Contact Us

Configuring Locations in Angie: Separating Dynamic and Static Requests

This article covers the basics of web server configuration in Angie: the location directive. It appears in every configuration that works with the HTTP module and is responsible for splitting request-handling settings by URL. location is also what separates requests into static and dynamic. We'll start with the latter — what dynamic and static requests actually are.

Video version

A video version of this article is available on YouTube.

Dynamic and static requests

The standard web application architecture today is a reverse proxy scheme (sometimes called a "front server") plus an application server ("backend"). For example, an Angie and PHP-FPM pairing.

In this scheme the front server handles static requests — serving files from disk — while the backend executes server-side code (PHP, for example) and hands the result back to the front to respond to the client.

Separating a location for static content

From the client side there is no way to tell static and dynamic requests apart with certainty, but for most web applications the following can be treated as static requests:

  • CSS files;
  • JS files;
  • embedded font files;
  • images;
  • audio and video files;
  • static text files (txt, html, xml);
  • other downloadable files (archives, binaries, etc.)

So to configure a web application correctly in Angie, each of these categories needs its own location that defines how such requests are handled. For example, if the application has a /js directory holding JS resources, its configuration might look like this:

location /js/ {
  root /var/www/html;
}

This is a so-called prefix location: all requests (unless a higher-priority location exists) whose address starts with /js/ are handled with this configuration. The root directive is shown here for clarity; in practice root can be defined at the server level and the block left empty, because the directive value is inherited from the higher level.

In some applications the directory and resource address structure is not reliable. Static files may sit anywhere in the address structure of the application. In that case a regular expression (regex) location applies. Typically static detection works off file extensions in the URL:

location ~* \.(?:css|js|woff2?|jpe?g|gif|png|txt|pdf)$ {
  root /var/www/html;
}

Here the resource address is matched against a case-insensitive regular expression (~*): at the end of the address ($) there must be a dot (\.) followed by one of the extensions. Note that GET parameters are not part of the address, so the request /1.css?test=2 also lands in this location.

The main drawback of this approach: every possible static file extension has to be accounted for in advance.

Separating a location for dynamic content

Dynamic requests are generally covered by the lowest-priority location /.. For example:

location / {
   proxy_pass http://127.0.0.1:9090/;
}

This describes all static requests, and everything else goes to the backend. A second way to single out dynamic requests is a regular expression on script file extensions:

location ~ \.php$ {
    fastcgi_pass unix:/run/php/php7.4-fpm.sock;
    fastcgi_index index.php;
    include fastcgi_params;
}

Such a location is reached either by a direct script call (/info.php), or when a request to a directory (/shop/) is processed with the index index.php directive. Modern web applications, though, allow arbitrary (human-readable) URLs, so it is hard to build a correct set of location blocks for them, and hard to tell a static request from a dynamic one. The try_files directive helps out: it checks whether files matching the requested address exist on disk and, when they do not, passes processing to another location or straight to a script:

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

location / {
try_files $uri $uri/ @bitrix;
}

location @bitrix {
        fastcgi_pass    127.0.0.1:9001;
        include fastcgi.conf;
}

In these examples the request lands in location /, where it is first tried as a URI, then as a directory, and then an internal redirect sends it to the script (/index.php?$args) or to a named location (@bitrix).

There is one more way to split dynamic and static requests, using if. if blocks are generally discouraged, because they behave unpredictably in combination with directives from other modules (other than rewrite), but this construction is possible too:

location / {
   root /var/www;
   index index.php;

   if (!-e $request_filename) {
      rewrite ^(.+)$ /index.php?q=$1 last;
   }
}

In the example above, if checks whether the file exists. If the file is there, it is served as a static request. If the file is not there, the request is redirected to /index.php?q=$1.

A correct configuration requires understanding the priorities of the different location types. That is what the next section covers.

Location priorities

We have already used several location types: prefix and regular expression. There are a few more ways to define a location, and they have different priorities. Understanding those priorities is required to configure Angie correctly.

The location types in Angie.

  1. Exact: location = /test/.
  2. Prefix: location /test/.
  3. Prefix without regex checking: location ^~ /test/.
  4. Case-insensitive regular expression: location ~* /test/.
  5. Case-sensitive regular expression: location ~ /test/.
  6. Named location: location @fallback.

The overall location search algorithm consists of the following steps.

  1. Search for an exact location (=).
  2. If none is found, look for matches among prefix locations. The longest match wins.
  3. If none was found, or the found location allows regular expression checking (that is, it is neither = nor ^~), check the regular expressions in order until the first match.
  4. If there is a match, use it; otherwise use the implicit location at the current level.

Inside regular expressions the first match wins, so the order of location entries in the config matters. Named locations (@back) are not in the priority list, because you can only get into them through an internal redirect.

When choosing location types, prefer prefix locations: they are more predictable and easier for the expression processor to check. If regular expressions are still necessary, add anchors (^ for the start of the line, $ for the end of the line) for more precise matching.

A typical web application configuration contains many location blocks with identical settings; the next section shows how to handle that more cleanly.

Copying configuration into locations

Suppose prefix locations were chosen for static content. The result will probably look roughly like this:

location /images/ {
    root /var/www;
    add_header Cache-Control "max-age=31536000, immutable";
}

location /css/ {
    root /var/www;
    add_header Cache-Control "max-age=31536000, immutable";
}

location /js/ {
    root /var/www;
    add_header Cache-Control "max-age=31536000, immutable";
}

location /files/ {
    root /var/www;
    add_header Cache-Control "max-age=31536000, immutable";
}

As can be seen, every location has identical directives. If root has the same value across the whole server, that directive can be moved up a level. The cache header is only needed in static locations, so for convenience let's move it into a separate file and pull it in with include. Optimized configuration:

location /images/ {
    include /etc/angie/static.conf;
}

location /css/ {
        include /etc/angie/static.conf;
}

location /js/ {
        include /etc/angie/static.conf;
}

location /files/ {
    include /etc/angie/static.conf;
}

Now recall that we are using Angie, which lets us write a combined location — several locations defined in a single block. The result reads better:

location /files/
    /css/
    /js/
    /images/ {
    include /etc/angie/static.conf;
}

In this configuration, changing the cache header only requires editing the file /etc/angie/static.conf and updating the configuration. The header file can also be used in other server blocks.

Nested locations

Angie supports nested location blocks, which can be used to isolate the effect of a configuration. For example, the scope of a regular expression can be limited by a prefix location. Consider the example below.

location /upload {
    root /var/www/static;

    location ~ \.php$ {
        deny all;
    }
}

location ~ \.php$ {
    fastcgi_pass unix:/run/php/php8.2-fpm.sock;
    fastcgi_index  index.php;
    include fastcgi.conf;
}

Here .php file handling is enabled by a regular expression, while requests to such files inside /upload are blocked.

Nested locations can be used in various combinations, but there are restrictions: an exact location (=) cannot contain nested blocks, and a named location (@) can neither be nested nor contain nesting.

This covered several ways to separate static and dynamic requests in Angie, the location types, and their priorities.