location blocks decide how Nginx handles each URL: serve a file, pass to PHP, proxy to an app, deny access, set caching headers. Most Nginx confusion comes from not knowing which location wins when several could match. The rules are precise and learnable.
The modifiers
location = /favicon.ico { … } # exact match
location ^~ /static/ { … } # prefix match; if it's the longest prefix, skip regexes
location ~ \.php$ { … } # case-sensitive regex
location ~* \.(jpg|png|webp)$ { … } # case-insensitive regex
location /api/ { … } # plain prefix match
location / { … } # prefix match on everything (catch-all)
How Nginx picks one
For each request URI (without the query string), Nginx:
- Checks exact matches (
=). If one matches, it’s used immediately. - Finds the longest matching prefix among all prefix locations (plain and
^~) and remembers it. - If that longest prefix has
^~, it’s used — regexes are skipped. - Otherwise, checks regex locations (
~,~*) in the order they appear in the config. The first match wins. - If no regex matches, uses the remembered longest prefix.
Key consequences:
- Regex locations beat plain prefix locations, regardless of length.
- Among regexes, order in the file matters; among prefixes, length matters.
^~is how you protect a prefix from being overridden by regexes.
A worked example
location / { … } # A
location /images/ { … } # B
location ^~ /static/ { … } # C
location ~* \.(png|jpg)$ { … } # D
location = /images/logo.png { … } # E
| Request | Winner | Why |
|---|---|---|
/about |
A | Only prefix that matches |
/images/cat.gif |
B | Longest prefix; no regex matches .gif |
/images/cat.png |
D | Regex beats prefix B |
/static/icon.png |
C | ^~ stops regex checking |
/images/logo.png |
E | Exact match wins first |
try_files
try_files checks for files in order and falls back to the last argument:
location / {
try_files $uri $uri/ =404; # static site
}
location / {
try_files $uri $uri/ /index.php?$args; # WordPress, Laravel and most PHP apps
}
location / {
try_files $uri /index.html; # single-page app (React, Vue)
}
The final argument is either a status code (=404) or an internal redirect to a URI, which then goes through location matching again (e.g. into your \.php$ block).
Common patterns
PHP
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
}
Long cache for static assets
location ~* \.(css|js|svg|woff2|jpg|jpeg|png|webp|avif|ico)$ {
expires 30d;
add_header Cache-Control "public";
access_log off;
}
Reverse proxy a path to an app
location /api/ {
proxy_pass http://127.0.0.1:3000/;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
Note the trailing slashes: with proxy_pass http://127.0.0.1:3000/; (URI part present), /api/users is forwarded as /users. With proxy_pass http://127.0.0.1:3000; (no URI), it’s forwarded as /api/users. This detail causes many 404s.
Block sensitive files
location ~ /\.(?!well-known) { deny all; } # .git, .env, .htaccess
location ~* \.(sql|bak|log|ini)$ { deny all; }
location ~* /uploads/.*\.php$ { deny all; } # no PHP execution in uploads
Regex order matters: put deny rules before the general \.php$ block, or a request like /uploads/shell.php will match the PHP location first.
Restrict an admin area by IP
location ^~ /admin/ {
allow 203.0.113.10;
deny all;
try_files $uri $uri/ /index.php?$args;
}
Exact-match health check
location = /health {
access_log off;
return 200 "ok\n";
}
Nested locations
Locations can be nested inside prefix locations:
location /app/ {
location ~ \.php$ {
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
include snippets/fastcgi-php.conf;
}
}
Useful for applying settings to a subtree, but keep nesting shallow for readability.
Inheritance gotcha: add_header
Directives like add_header are inherited from the parent level only if the current level has none of its own. Adding one add_header in a location drops all the security headers defined at server level for that location. Repeat them, or move them into an included snippet.
Debugging which location matched
sudo nginx -Tprints the full config so you can read all locations in order.- Temporarily add a header in candidate locations:
add_header X-Location "static" always;and check withcurl -I. - Enable debug logging (
error_log … debug;) on a test server for the full decision trace.
Quick reference
| Syntax | Type | Priority |
|---|---|---|
= /path |
Exact | 1 (highest) |
^~ /path |
Prefix, stops regex search | 2 (if longest prefix) |
~ regex / ~* regex |
Regex, first in file order wins | 3 |
/path |
Prefix, longest wins | 4 |
More Nginx: server blocks, rewrite rules and the 403 Forbidden fix.