Skip to content
DevOps devops webservers 5 min read

Nginx Location Blocks

When a request reaches Nginx (a popular, fast web server and reverse proxy), Nginx has to decide what to do with the URL the visitor asked for. Should it return a file from disk? Forward the request to your app? Return a redirect? The location block is how you make that decision. A location block matches part of the request URL (called the URI, the path after the domain, like /api/users) and tells Nginx exactly how to handle requests for that path. Getting location matching right is one of the most important Nginx skills, because the matching rules are subtle and a wrong choice silently sends requests to the wrong place.

This page targets Ubuntu 22.04/24.04 LTS, where Nginx config lives under /etc/nginx, sites under /etc/nginx/sites-available, and logs under /var/log/nginx.

What a location block looks like

A location block lives inside a server block (the part of your config that handles one website). Each location says “for URLs that match this pattern, do this”:

server {
    listen 80;
    server_name example.com;

    location /images/ {
        root /var/www/example;
    }
}

Here, a request for https://example.com/images/logo.png matches /images/, and Nginx serves the file /var/www/example/images/logo.png. The path is built from root + the full URI.

The three kinds of matching

Nginx supports several match types, and which one you pick changes both what matches and which block wins when several could match.

ModifierNameMatches whenCase sensitive
(none)PrefixURI starts with the stringn/a
=ExactURI equals the string exactlyn/a
~RegexURI matches the regular expressionYes
~*RegexURI matches the regular expressionNo
^~Prefix (stop)URI starts with the string, and skip regex checksn/a

A prefix match (no modifier) matches any URI that begins with the string. location /api { ... } matches /api, /api/, and /api/users.

An exact match (=) matches only when the URI is identical. location = / { ... } matches the homepage / and nothing else. Exact matches are the fastest, so they are great for a hot path like the homepage or a health check.

A regex match (~ for case-sensitive, ~* for case-insensitive) uses a regular expression (a pattern language for matching text). location ~* \.(jpg|png|gif)$ { ... } matches any URI ending in those image extensions, in any letter case.

The precedence order (the confusing part)

This is where most people get tripped up. Nginx does not simply pick the first matching block in the file, and it does not always pick the longest match. The real order is:

  1. Nginx checks all = (exact) matches first. If one matches, it is used immediately and matching stops.
  2. Nginx then checks prefix matches and remembers the longest matching prefix.
  3. If that longest prefix used the ^~ modifier, Nginx uses it and stops — no regex checks.
  4. Otherwise, Nginx checks regex (~ and ~*) blocks in the order they appear in the file. The first regex that matches wins.
  5. If no regex matches, Nginx falls back to the longest prefix it remembered in step 2.

So the summary is: exact wins, then regex (unless a ^~ prefix blocked it), then longest prefix.

Gotcha: a more specific-looking prefix can lose to a regex. If you have location /downloads/ { ... } and location ~ \.zip$ { ... }, a request for /downloads/file.zip is served by the regex block, not the prefix block — because regex outranks an ordinary prefix. Use ^~ on the prefix (location ^~ /downloads/ { ... }) when you want the prefix to win and stop regex checks.

Serving files with try_files

try_files checks a list of files or paths in order and uses the first one that exists. It is the standard way to serve a static site and to power single-page apps (apps where one HTML file handles routing in the browser).

location / {
    root /var/www/example;
    try_files $uri $uri/ /index.html;
}

This means: try the exact file ($uri), then try it as a directory ($uri/), and if neither exists, fall back to /index.html. The fallback is what lets a React or Vue app handle its own routes. The final argument can also be a status code, like try_files $uri =404; to return a clean 404 when a file is missing.

A real config: static plus proxy

Here is a complete, working site that serves static files directly and forwards /api requests to an app running on port 3000. This is the most common real-world pattern.

# /etc/nginx/sites-available/example.com
server {
    listen 80;
    server_name example.com;

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

    # Exact match for a fast health check
    location = /healthz {
        return 200 "ok\n";
        add_header Content-Type text/plain;
    }

    # Long-cache static assets; ^~ stops regex from stealing these
    location ^~ /static/ {
        expires 30d;
        access_log off;
    }

    # Proxy API calls to the Node/Python app
    location /api/ {
        proxy_pass http://127.0.0.1:3000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    # Everything else: serve the SPA
    location / {
        try_files $uri $uri/ /index.html;
    }
}

Enable the site and reload Nginx safely:

sudo ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

Output:

nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

Test that each path goes where you expect:

curl -i http://example.com/healthz
curl -i http://example.com/api/users

Output:

HTTP/1.1 200 OK
Content-Type: text/plain
Content-Length: 3

ok

Always run sudo nginx -t before systemctl reload nginx. A bad config will fail the test, and reload keeps the old working config running instead of taking down your site. Never use restart for routine changes — reload applies new config with zero downtime.

Best practices

  • Use = exact matches for hot, single-URL paths like /, /favicon.ico, and health checks — they are the fastest and skip all other checks.
  • Use ^~ on a static-asset prefix (like /static/ or /assets/) so a regex block can never accidentally intercept those files.
  • Order your regex blocks deliberately, because the first matching regex wins, not the most specific one.
  • Always end a catch-all try_files with a sensible fallback (/index.html for SPAs, or =404 for plain static sites).
  • Keep proxy_set_header lines on proxied locations so your backend app sees the real client IP and protocol.
  • Test changes with curl -i against each path before trusting them, and confirm nginx -t passes on every edit.
Last updated June 15, 2026
Was this helpful?