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:

  1. Checks exact matches (=). If one matches, it’s used immediately.
  2. Finds the longest matching prefix among all prefix locations (plain and ^~) and remembers it.
  3. If that longest prefix has ^~, it’s used — regexes are skipped.
  4. Otherwise, checks regex locations (~, ~*) in the order they appear in the config. The first match wins.
  5. 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 -T prints 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 with curl -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.

  • Nginx
  • Web Server
  • Configuration