Fix: Nginx location Block Not Matching (Wrong Route Served)
Quick Answer
How to fix Nginx location blocks not matching — caused by prefix vs regex priority, trailing slash issues, root vs alias confusion, and try_files misconfiguration.
The Error
You add or modify a location block in your Nginx config but it never matches — requests go to a different block or return 404. There is no error message in the Nginx logs; the wrong block simply handles the request silently.
Common symptoms:
- A
location /api/block is configured but requests to/api/usersare handled bylocation /instead. - A regex location like
location ~ \.php$does not trigger for.phpfiles. - Static files are served correctly but SPA routes return 404.
- Adding a new
locationblock has no effect after reloading Nginx. location /adminserves the wrong root directory.- A redirect inside a
locationblock loops infinitely.
Why This Happens
Nginx evaluates location blocks using a specific precedence order that does not match intuition. The most common mistakes:
- Misunderstanding location priority — exact match (
=), prefix match (^~), regex match (~,~*), and standard prefix match (/) have a specific evaluation order. - Missing trailing slash —
location /apiandlocation /api/behave differently. rootvsalias—rootappends the location path;aliasreplaces it. Mixing them up serves files from the wrong directory.- Config not reloaded — changes are made but
nginx -s reloadwas not run. - Regex syntax errors — a malformed regex silently falls through to the next matching block.
try_filesmisconfiguration — SPA routing requirestry_files $uri $uri/ /index.htmlbut a wrong fallback causes 404.
How Nginx Location Matching Works
Understanding priority is essential. Nginx evaluates locations in this order:
- Exact match (
=):location = /favicon.ico— matches only that exact URI. - Preferential prefix (
^~):location ^~ /images/— if matched, stops evaluating regex blocks. - Regex match (
~case-sensitive,~*case-insensitive): evaluated in the order they appear in the config. - Longest prefix match (no modifier): the longest matching prefix wins, but only if no regex matches.
server {
location = /exact {
# Matches only GET /exact — highest priority
}
location ^~ /api/ {
# Matches /api/... — prevents regex from running if this matches
}
location ~ \.php$ {
# Regex — runs if no exact or ^~ match
}
location / {
# Catch-all prefix — lowest priority
}
}Why this matters: Many developers assume Nginx reads locations top-to-bottom like a list of if-else statements. It does not. The longest prefix match wins among prefix locations, and regex locations are tried in order but only after all prefix locations are evaluated. A location earlier in the file can lose to a longer prefix location later in the file.
Fix 1: Use = for Exact URI Matches
If you need a specific URI handled differently, use exact match to guarantee it:
# Without =, /health might match a longer prefix location first
location /health {
return 200 "ok";
}
# With =, this always wins for exactly /health
location = /health {
return 200 "ok";
add_header Content-Type text/plain;
}Exact match is also the most efficient — once Nginx finds an exact match, it stops searching immediately.
Fix 2: Fix Prefix Location Priority with ^~
If you have a prefix location that should take priority over regex locations, add ^~:
Broken — regex catches static files before the prefix location:
location /static/ {
root /var/www;
expires 1y;
}
location ~ \.(jpg|png|css|js)$ {
# This matches /static/logo.png too, and may override the /static/ block
add_header Cache-Control "public";
}Fixed — use ^~ to block regex evaluation:
location ^~ /static/ {
root /var/www;
expires 1y;
# Regex locations are NOT checked for anything starting with /static/
}
location ~ \.(jpg|png|css|js)$ {
add_header Cache-Control "public";
# Only applies to files NOT under /static/
}Fix 3: Fix root vs alias Confusion
root and alias serve files from the filesystem, but they work differently:
root: appends the location URI to the root path.location /images/+root /var/www→ serves from/var/www/images/.alias: replaces the location URI with the alias path.location /images/+alias /var/www/static/→ serves from/var/www/static/.
Broken — using root when alias is needed:
location /assets/ {
root /var/www/static;
# Request: /assets/logo.png
# Nginx looks for: /var/www/static/assets/logo.png ← WRONG
# The path is doubled!
}Fixed — use alias:
location /assets/ {
alias /var/www/static/;
# Request: /assets/logo.png
# Nginx looks for: /var/www/static/logo.png ← CORRECT
}Rule of thumb: Use root when the location URI and the directory name match. Use alias when they differ.
Common Mistake: When using
alias, always end both thelocationpath and thealiaspath with a trailing slash, or neither. Mismatched trailing slashes cause subtle path errors.location /assets/withalias /var/www/static/is correct.location /assetswithalias /var/www/staticis also correct. Mixing (location /assets/withalias /var/www/static) is broken.
Fix 4: Fix SPA Routing with try_files
Single-page applications (React, Vue, Angular) need Nginx to serve index.html for all routes, so the frontend router can handle them:
Broken — 404 on direct URL access:
location / {
root /var/www/app;
index index.html;
# Direct access to /dashboard returns 404 because there's no /dashboard file
}Fixed — add try_files fallback:
location / {
root /var/www/app;
index index.html;
try_files $uri $uri/ /index.html;
}try_files $uri $uri/ /index.html tells Nginx to:
- Try the exact file (
$uri). - Try as a directory (
$uri/). - Fall back to
/index.htmlif neither exists.
For APIs coexisting with an SPA:
location /api/ {
proxy_pass http://localhost:3000/;
# API requests go to Node.js backend
}
location / {
root /var/www/app;
try_files $uri $uri/ /index.html;
# Everything else serves the SPA
}The /api/ location is more specific than /, so API requests are correctly routed to the backend.
Fix 5: Fix Trailing Slash Issues
A trailing slash matters in location blocks:
location /apimatches/api,/api/,/api/users,/apiv2(any URI starting with/api).location /api/matches/api/,/api/users— but NOT/apialone or/apiv2.
Redirect bare path to trailing slash version:
location = /app {
return 301 /app/;
}
location /app/ {
root /var/www;
try_files $uri $uri/ /app/index.html;
}In proxy_pass, trailing slash changes path stripping:
# Without trailing slash on proxy_pass:
location /api/ {
proxy_pass http://localhost:3000;
# Request: /api/users → proxied as /api/users (full path kept)
}
# With trailing slash on proxy_pass:
location /api/ {
proxy_pass http://localhost:3000/;
# Request: /api/users → proxied as /users (strips /api/)
}The trailing slash on proxy_pass strips the location prefix from the proxied URL. This is usually what you want when the backend does not know about the /api/ prefix.
Fix 6: Test Location Matching Without Restarting
Use nginx -T to dump the full resolved config and look for unexpected values:
sudo nginx -T | grep -A 10 "location /api"Use nginx -t to test configuration syntax before reloading:
sudo nginx -t
# nginx: configuration file /etc/nginx/nginx.conf test is successfulUse curl to test which location is handling requests:
Add a temporary header to identify each location:
location /api/ {
add_header X-Location "api-block" always;
proxy_pass http://localhost:3000/;
}
location / {
add_header X-Location "default-block" always;
root /var/www/app;
try_files $uri /index.html;
}Then check which block responds:
curl -I http://localhost/api/users
# Look for: X-Location: api-blockRemove these debug headers before going to production.
Fix 7: Reload Nginx After Every Config Change
Changes to Nginx config have no effect until you reload or restart:
# Test config first (always do this before reloading)
sudo nginx -t
# Reload without downtime (graceful)
sudo nginx -s reload
# Or via systemctl
sudo systemctl reload nginx
# Full restart (causes brief downtime)
sudo systemctl restart nginxnginx -s reload sends a SIGHUP to the master process. It reads the new config, starts new worker processes with the new config, and gracefully shuts down old workers after they finish their current requests. Zero downtime.
Verify the reload took effect:
sudo systemctl status nginx
# Check the timestamp — should show the recent reload timeFix 8: Debug with Nginx Error and Access Logs
The access log shows which location handled each request (with the right log format):
log_format detailed '$remote_addr - $request - status:$status - upstream:$upstream_addr';
access_log /var/log/nginx/access.log detailed;The error log shows why a location failed to match or returned an error:
# Watch logs in real time
sudo tail -f /var/log/nginx/error.log
sudo tail -f /var/log/nginx/access.log
# Filter for specific path
sudo tail -f /var/log/nginx/access.log | grep "/api/"For 502 errors after location blocks successfully route to an upstream, see Fix: Nginx 502 Bad Gateway. For 403 errors on static files, see Fix: Nginx 403 Forbidden.
Still Not Working?
Check for config file includes. Nginx often includes files from /etc/nginx/conf.d/*.conf or /etc/nginx/sites-enabled/. A location in an included file may override your changes. Run sudo nginx -T to see the fully resolved config.
Check for conflicting server blocks. If you have multiple server blocks, make sure the request is reaching the right one. Nginx matches server blocks by server_name and port. Add default_server to the block that should catch requests when no other server name matches.
Check for if blocks overriding locations. if inside a location can behave unexpectedly in Nginx. The Nginx documentation warns that if is “evil” in certain contexts. Replace if with map, geo, or separate location blocks where possible.
Check for inherited directives. Some directives (like try_files) in a parent location do not apply to nested locations. Each location block that needs try_files must define it explicitly.
For upstream timeout issues that look like location mismatches, see Fix: Nginx 504 Gateway Timeout.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: Certbot Certificate Renewal Failed (Let's Encrypt)
How to fix Certbot certificate renewal failures — domain validation errors, port 80 blocked, nginx config issues, permissions, and automating renewals with systemd or cron.
Fix: Linux OOM Killer Killing Processes (Out of Memory)
How to fix Linux OOM killer terminating processes — reading oom_kill logs, adjusting oom_score_adj, adding swap, tuning vm.overcommit, and preventing memory leaks.
Fix: Kubernetes Ingress Not Working (404, 502, or Traffic Not Routing)
How to fix Kubernetes Ingress not routing traffic — why Ingress returns 404 or 502, how to configure annotations correctly, debug ingress-nginx and AWS ALB Ingress Controller, and verify backend service health.
Fix: Docker Compose Environment Variables Not Loading from .env File
How to fix Docker Compose not loading environment variables from .env files — why variables are empty or undefined inside containers, the difference between env_file and variable substitution, and how to debug env var issues.