Location Block #
If the Server Block dictates which machine or domain should handle an incoming connection, then the Location Block dictates what specific action should be taken for that request based on its URI path (the URL after the domain name). The location block is where most of your application’s routing logic lives — from serving static files, protecting admin directories, rewriting URLs, to forwarding traffic to backend machines (reverse proxying).
Although location blocks look simple, they have a very complex and strict matching engine. A small mistake in choosing a modifier symbol or the order of regex writing can cause sensitive configuration files (like .env files) to leak to the internet, or PHP scripts to be executed as plain text. This article covers matching modifiers, unpacks Nginx’s priority search algorithm, explains try_files implementation, and dissects the crucial difference between the root and alias directives.
The Five Matching Modifiers: Syntax & How They Work #
Inside Nginx, a modifier is a special symbol placed right after the location keyword and before the path search string. This symbol determines the string comparison behavior Nginx performs:
location modifier /path/target/ {
# directive ...
}
Here’s an in-depth explanation of the five modifier types Nginx supports:
1. No Modifier (Plain Prefix Match) #
If you write a location without any symbol, Nginx performs a prefix match.
- Behavior: Request URLs that start with the target string match.
- Example:
location /images/ { ... }- ✓ Matches:
/images/,/images/logo.png,/images/temp/photo.jpg. - ✗ Doesn’t match:
/my-images/,/images(without trailing slash).
- ✓ Matches:
2. = (Exact Match)
#
The equals = modifier tells Nginx to compare the request URI character-by-character with the target.
- Behavior: Only matches if the request URI is exactly identical to the target string. Once matched, Nginx immediately stops all other routing searches.
- Example:
location = / { ... }- ✓ Matches:
/only. - ✗ Doesn’t match:
/index.html,/about. - Best Practice: Use this modifier to speed up processing of the most frequently accessed URLs (like the home page
/or the/favicon.icofile) to avoid memory search traversal.
- ✓ Matches:
3. ^~ (High-Priority Prefix Match)
#
The ^~ modifier works like a regular prefix match (matching the beginning), but has a special protective property.
- Behavior: If a request matches this prefix, and this prefix proves to be the longest matching prefix in the system, Nginx immediately uses this block and ignores all regex searches (
~or~*). - Example:
location ^~ /static/ { ... }- Why does this matter? Imagine you have a
/static/folder containing CSS assets and images, and you also have a PHP regex handlerlocation ~ \.php$. If a user requests the file/static/avatar.php(possibly injected by a hacker), Nginx without^~would pick the PHP handler because regex has higher priority than prefix. With^~, Nginx directly serves that static file without executing it as PHP, preventing code execution attacks.
- Why does this matter? Imagine you have a
4. ~ (Case-Sensitive Regular Expression)
#
Uses case-sensitive regular expression (regex) matching.
- Behavior: Uses the PCRE regex engine to evaluate URLs.
- Example:
location ~ \.php$ { ... }- ✓ Matches:
/index.php,/user/profile.php. - ✗ Doesn’t match:
/index.PHP(uppercase).
- ✓ Matches:
5. ~* (Case-Insensitive Regular Expression)
#
Uses case-insensitive regex matching.
- Behavior: Same as
~, but ignores character capitalization differences. - Example:
location ~* \.(jpg|jpeg|png|gif|ico)$ { ... }- ✓ Matches:
/logo.png,/banner.JPG,/avatar.Png.
- ✓ Matches:
The Matching Priority Algorithm (Routing Priority) #
When a request comes in, Nginx may find several location blocks that could potentially match the URI. Nginx uses an internal priority algorithm to guarantee deterministic matching results (always producing the same decision).
Here’s the systematic decision flow Nginx goes through to select a location block:
flowchart TD
Start["Request URI Arrives (e.g., /assets/app.js)"] --> Step1{"1. Is there an Exact (=) Match <br> for this URI?"}
Step1 -->|"Yes"| ReturnExact["Use the Exact (=) Block <br> (Process Done)"]
Step1 -->|"No"| Step2["2. Search all plain Prefix Matches <br> (no modifier & ^~). <br> Record the longest matched prefix."]
Step2 --> Step3{"3. Does the longest recorded <br> prefix have a ^~ modifier?"}
Step3 -->|"Yes"| ReturnPriority["Use the Priority Prefix (^~) Block <br> (Ignore Regex - Done)"]
Step3 -->|"No"| Step4{"4. Search for Regex matches (~ and ~*) <br> in the order written in the config file."}
Step4 -->|"Regex Match Found"| ReturnRegex["Use the First matching Regex Block <br> (Done)"]
Step4 -->|"No Regex Match"| ReturnFallback["5. Use the longest Prefix Match <br> recorded in step 2 as fallback"]
style Start stroke:#0288d1,stroke-width:2.5px
style Step3 stroke:#f57c00,stroke-width:2px
style ReturnExact stroke:#388e3c,stroke-width:1.5px
style ReturnPriority stroke:#d32f2f,stroke-width:2pxReal-World Simulation Example #
Let’s test the algorithm above using the following server block configuration:
server {
location / {
# Block A (Fallback)
}
location = /favicon.ico {
# Block B (Exact Match)
}
location ^~ /static/ {
# Block C (Priority Prefix)
}
location ~* \.(js|css)$ {
# Block D (Asset File Regex)
}
location ~ \.php$ {
# Block E (PHP Regex)
}
}
Let’s simulate several request URIs arriving at the server:
- Request
GET /favicon.ico- Result: Block B is chosen. Reason: Block B uses the exact match modifier (
=), selected directly in Step 1.
- Result: Block B is chosen. Reason: Block B uses the exact match modifier (
- Request
GET /static/js/app.js- Result: Block C is chosen. Reason: Block C matches the longest prefix
/static/. Because it uses^~, Nginx stops immediately and doesn’t evaluate Block D (JS Regex), even though the file ends in.js.
- Result: Block C is chosen. Reason: Block C matches the longest prefix
- Request
GET /assets/style.css- Result: Block D is chosen. Reason: The longest matching prefix is
/(Block A), which doesn’t use^~. Nginx continues evaluating regexes and finds Block D matches the.cssending.
- Result: Block D is chosen. Reason: The longest matching prefix is
- Request
GET /index.php- Result: Block E is chosen. Reason: Nginx skips exact and priority prefix matches, then evaluates regexes and finds Block E matches the
.phpending.
- Result: Block E is chosen. Reason: Nginx skips exact and priority prefix matches, then evaluates regexes and finds Block E matches the
- Request
GET /about-us- Result: Block A is chosen. Reason: No exact, priority prefix, or regex matches. Nginx uses the longest matched prefix fallback, which is
/.
- Result: Block A is chosen. Reason: No exact, priority prefix, or regex matches. Nginx uses the longest matched prefix fallback, which is
Named Locations (@)
#
Nginx supports creating Named Locations marked with the @ character prefix. These named location blocks aren’t used to serve external HTTP requests directly; instead, they act as internal sub-functions called by other directives like try_files or error_page.
server {
listen 80;
server_name example.com;
location / {
# Try to serve a static file from disk.
# If the file doesn't exist, throw the request to the @api_backend named location.
try_files $uri $uri/ @api_backend;
}
location @api_backend {
proxy_pass http://localhost:8080;
proxy_set_header Host $host;
# ...
}
}
- Advantage: Using a named location is faster and more memory-efficient than doing a regular internal redirect (using a path like
/fallback/), because Nginx doesn’t need to repeat the location block search cycle from scratch.
Implementing try_files for SPAs and PHP MVC #
The try_files directive is an essential tool inside location blocks that instructs Nginx to search for physical files on disk in order, and use the first file found.
The try_files syntax:
try_files file1 [file2 ...] fallback_uri_or_named_location;
Here are two try_files implementations you must master for modern application architectures:
1. Single Page Application Router Pattern (React / Vue / Angular) #
In SPA applications, the client browser handles all page routing client-side using JavaScript (client-side routing). If a user refreshes the page at example.com/dashboard/users, Nginx physically doesn’t have that file on disk.
server {
listen 80;
server_name example.com;
root /var/www/my-spa/dist;
location / {
# 1. Try to find the real file (e.g., /assets/app.js)
# 2. Try to find the directory (e.g., /assets/)
# 3. If not found, always return index.html
try_files $uri $uri/ /index.html;
}
}
With the configuration above, Nginx serves the main index.html file for all unknown URLs, letting the JavaScript application load in the browser and handle internal page routing on its own.
2. PHP Front Controller Pattern (Laravel / WordPress) #
Modern PHP frameworks route all incoming requests through one main gateway file, index.php.
server {
listen 80;
server_name example.com;
root /var/www/my-laravel/public;
index index.php;
location / {
# If the file/folder doesn't exist, redirect the request to index.php
# appending the original query string ($query_string)
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
include fastcgi_params;
fastcgi_pass unix:/run/php/php8.1-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
}
The Crucial Difference: root vs alias #
One of the most common mistakes in Nginx configuration is swapping the root and alias directives inside a location block. This mistake leads to 404 Not Found errors because Nginx looks for files in the wrong disk location.
1. The root Directive (Path Concatenation)
#
The root directive sets the base directory for file lookups. Nginx concatenates the root path contents with the entire request URI string.
location /static/ {
root /var/www/assets;
}
- Resolution Flow: If there’s a request for
/static/css/style.css, Nginx concatenates them into: $$\text{Result Path} = \text{root} + \text{URI} = \text{/var/www/assets} + \text{/static/css/style.css}$$ $$\text{Result Path} = \text{/var/www/assets/static/css/style.css}$$ - Note: The
/static/sub-folder must exist inside/var/www/assets/.
2. The alias Directive (Path Replacement)
#
The alias directive performs a complete directory remapping. Nginx replaces the location prefix portion with the alias path.
location /static/ {
alias /var/www/assets/;
}
- Resolution Flow: If there’s a request for
/static/css/style.css, Nginx discards the/static/part from the URI and replaces it with the alias path: $$\text{Result Path} = \text{alias} + (\text{URI} - \text{prefix}) = \text{/var/www/assets/} + \text{css/style.css}$$ $$\text{Result Path} = \text{/var/www/assets/css/style.css}$$ - Note: The
/static/sub-folder doesn’t need to exist inside the target directory/var/www/assets/.
Quick Comparison Matrix: #
| Comparison Dimension | root Directive | alias Directive |
|---|---|---|
| Search Method | Concatenates the path with the entire URI. | Replaces the prefix string with the alias path. |
Trailing Slash / | Not sensitive to the closing slash. | Very Sensitive. Slashes on location and alias must align. |
| Compatibility | Can be used in all contexts. | Only valid inside a location block. |
| Regex Support | Supports regex safely. | Requires manual regex capture groups to work. |
The Off-by-One Slash Danger with alias! If you use
alias, make sure the trailing slash (/) at the end of thelocationandaliasstrings matches consistently:# ANTI-PATTERN: Causes file lookup failure due to a missing or extra slash location /static/ { alias /var/www/assets; # Missing the trailing slash! }If you request
/static/app.jswith the wrong configuration above, Nginx translates it to/var/www/assetsapp.js(without a directory separator), resulting in a404 Not Founderror. Always align the trailing slash: if the location has one, the alias must have one too.
Using alias Inside a Regular Expression Location Block #
A crucial thing to know is that, by default, Nginx doesn’t support using plain alias inside a regex-based location block. If you write:
# ANTI-PATTERN: Nginx doesn't know how to map a dynamic path to disk
location ~* ^/images/(.+\.(png|jpg))$ {
alias /var/www/data/media/;
}
The rule above triggers an error because Nginx can’t figure out which URI part should be discarded and replaced.
The Correct Solution (Using Capture Groups): #
If you’re forced to use alias inside a regex location, you must capture the desired path segment using regex parentheses (capture groups) and state it explicitly using variables like $1, $2, etc., in the alias directory:
# CORRECT: Capture the file name into the $1 variable
location ~* ^/images/(.+\.(png|jpg))$ {
alias /var/www/data/media/$1;
}
In this example, if there’s a request for /images/user-profile/avatar.png, Nginx captures the string user-profile/avatar.png into the $1 variable and maps the file lookup to /var/www/data/media/user-profile/avatar.png accurately.
Nested Locations: Safe Usage & Limitations #
Nginx allows creating location blocks inside other location blocks (nesting). Using nested locations is useful for isolating special handling for specific sub-directories.
Here’s a production-grade nested location configuration pattern:
server {
listen 80;
server_name example.com;
root /var/www/html;
# Outer Location: Handles the admin portal area
location /admin/ {
# Restrict access to the IT team's internal IPs only
allow 192.168.1.0/24;
deny all;
# Inner (Nested) Location: Handles admin-specific PHP files
location ~ \.php$ {
include fastcgi_params;
fastcgi_pass unix:/run/php/php8.1-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
}
}
- Why is this safe? Nested locations inherit the security restrictions (
allow/deny) from their outer block automatically. PHP files inside/admin/can only be accessed by allowed IPs, while PHP files outside/admin/remain publicly accessible (if there’s an outer PHP handler). - Warning: Limit nesting depth to a maximum of 2 levels. Too-deep nested location structures trigger complex memory read overhead and increase the risk of inheritance misconfiguration.
Anti-Patterns: The Regex Shadow Trap #
When writing many location blocks using regular expressions (~ or ~*), you must remember that Nginx evaluates regexes in the order they appear in the configuration file.
# ANTI-PATTERN: The second block will NEVER execute!
server {
# 1. Catches all files ending in .php
location ~ \.php$ {
fastcgi_pass unix:/run/php/php8.1-fpm.sock;
}
# 2. Special protection block for the admin area
location ~ /admin\.php$ {
allow 192.168.1.100; # Only the internal team IP can access
deny all;
fastcgi_pass unix:/run/php/php8.1-fpm.sock;
}
}
Why Does This Happen? #
If a user accesses /admin.php, Nginx scans the configuration from top to bottom. Nginx finds Block 1 (~ \.php$) matches the URI, executes it immediately, and stops searching. Block 2, which contains the IP security firewall rules, is never evaluated. As a result, the admin page leaks to the outside internet.
The Correct Solution: #
Write the most specific regex locations (strictest path patterns) higher than general global regexes:
# CORRECT: Order from most specific to most general
server {
# 1. Put the specific protection at the top
location ~ /admin\.php$ {
allow 192.168.1.100;
deny all;
fastcgi_pass unix:/run/php/php8.1-fpm.sock;
}
# 2. Fallback for regular PHP files
location ~ \.php$ {
fastcgi_pass unix:/run/php/php8.1-fpm.sock;
}
}
Summary #
- Location modifiers control routing logic:
=for exact, no modifier for prefix,^~for priority prefix (skip regex), and~/~*for regex.- Nginx’s priority algorithm favors exact match
=, then the longest matched prefix (if it has^~, stop immediately), then scans regexes in file order, and finally falls back to the longest plain prefix.- The
try_filesdirective is mandatory for supporting dynamic routing in SPA applications (React/Vue) and MVC Front Controllers (Laravel/WordPress).- Named locations (
@name) act as fast internal sub-functions for internal traffic redirection without triggering a rescan cycle.rootconcatenates the entire request URI with the root path, whilealiasreplaces the location prefix with the alias path.- Using
aliasinside regex must include captured variables (like$1) to dynamically point to disk file paths.- The order of writing regexes matters; put more specific regex blocks above general ones to avoid rule shadowing (regex shadow).