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.
| Modifier | Name | Matches when | Case sensitive |
|---|---|---|---|
| (none) | Prefix | URI starts with the string | n/a |
= | Exact | URI equals the string exactly | n/a |
~ | Regex | URI matches the regular expression | Yes |
~* | Regex | URI matches the regular expression | No |
^~ | Prefix (stop) | URI starts with the string, and skip regex checks | n/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:
- Nginx checks all
=(exact) matches first. If one matches, it is used immediately and matching stops. - Nginx then checks prefix matches and remembers the longest matching prefix.
- If that longest prefix used the
^~modifier, Nginx uses it and stops — no regex checks. - Otherwise, Nginx checks regex (
~and~*) blocks in the order they appear in the file. The first regex that matches wins. - 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/ { ... }andlocation ~ \.zip$ { ... }, a request for/downloads/file.zipis 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 -tbeforesystemctl reload nginx. A bad config will fail the test, andreloadkeeps the old working config running instead of taking down your site. Never userestartfor routine changes —reloadapplies 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_fileswith a sensible fallback (/index.htmlfor SPAs, or=404for plain static sites). - Keep
proxy_set_headerlines on proxied locations so your backend app sees the real client IP and protocol. - Test changes with
curl -iagainst each path before trusting them, and confirmnginx -tpasses on every edit.