Nginx Location Priority and Fallback Routing Configuration for Multiple Applications


10 views

When migrating monolithic applications to microservices while maintaining legacy URL structures, Nginx location priority becomes critical. The specific pain point emerges when you need:

  • Explicit paths (/ and /community) handled by Perl scripts
  • /news route dedicated to WordPress
  • CakePHP as universal fallback

The current approach using location ~ .* as catch-all in the CakePHP config causes it to hijack all requests, including /news. This happens because:

# Problematic pattern - too greedy
location ~ .* {
  # This will match EVERY request
}

Here's the corrected configuration structure that respects proper matching order:

# /etc/nginx/sites-enabled/example.org
server {
    listen 80;
    server_name example.org;
    
    # Process most specific matches first
    location = / {
        include /etc/nginx/subsites-enabled/example.org/home;
    }
    
    location ^~ /community {
        include /etc/nginx/subsites-enabled/example.org/home;
    }
    
    location ^~ /news {
        include /etc/nginx/subsites-enabled/example.org/news;
    }
    
    # Fallback (must come LAST)
    location / {
        include /etc/nginx/subsites-enabled/example.org/app;
    }
}

1. Match priority rules:

  • = (exact match) has highest priority
  • ^~ (prefix match) comes next
  • Regular expressions (~) in order of declaration

2. Static assets protection:

location ~* \.(jpg|jpeg|png|gif|ico|css|js)$ {
    expires 365d;
    add_header Cache-Control "public";
    try_files $uri =404;
}

Add this to your server block to see matching behavior:

location /nginx-match-test {
    default_type text/plain;
    return 200 "Matched: $uri\nLocation: $request_uri\n";
}

Test patterns using:

curl -I http://example.org/nginx-match-test/community/subpage

Here's the complete optimized configuration for the home subsystem:

# /etc/nginx/subsites-enabled/example.org/home
location ~ ^/(|community(/.*)?)$ {
    access_log /var/log/nginx/home/access.log;
    
    try_files $uri @perlhandler;
    
    location ~ \.pl$ {
        internal;
        include fastcgi_params;
        fastcgi_pass unix:/var/run/fcgiwrap.socket;
        fastcgi_param SCRIPT_FILENAME /var/www/vhosts/home$fastcgi_script_name;
    }
}

location @perlhandler {
    rewrite ^ /index.pl last;
}

Always place static asset handling before dynamic content rules:

location ~* \.(?:css|js|map|jpe?g|png|gif|ico)$ {
    root /var/www/vhosts/app/app/webroot;
    access_log off;
    expires 30d;
}

When migrating from a monolithic repository to microservices while maintaining URL structures, Nginx location priority becomes critical. The specific requirement where one application must serve as a fallback for unmatched routes introduces complex routing logic.

The existing setup has three key location blocks across separate files:

# Home application (Perl)
location = / { ... }
location ~* /community(.*) { ... }

# News application (WordPress)
location /news { ... }

# Fallback application (CakePHP)
location ~ .* { ... }

The greedy regex pattern ~ .* in the fallback application catches all requests, including those meant for /news. Nginx evaluates locations in this order:

  1. Exact matches (=)
  2. Prefix matches (^~)
  3. Regular expressions (~ and ~*) in config file order
  4. Generic prefix matches

We need to:

# 1. Make home locations explicit
location = / { ... }
location ^~ /community { ... }

# 2. Prioritize news before fallback
location ^~ /news {
    # WordPress specific rules
    try_files $uri $uri/ /news/index.php?$args;
}

# 3. Revised fallback with negative lookahead
location / {
    # Exclude already handled paths
    if ($request_uri ~ ^/(news|community)) {
        return 404;
    }
    # CakePHP rewrite rules
    try_files $uri $uri/ /index.php?$args;
}

For more complex scenarios, consider:

# Option 1: Map-based routing
map $request_uri $app_root {
    default "/var/www/vhosts/app";
    ~^/news "/var/www/vhosts/news";
    ~^/community "/var/www/vhosts/home";
}

# Option 2: Named locations
location @fallback {
    # CakePHP processing
}

location / {
    try_files $uri @fallback;
}

Add these directives to monitor routing decisions:

log_format routing_debug '$remote_addr - $request [$location]';
set $location "default";
access_log /var/log/nginx/routing.log routing_debug;

location /news {
    set $location "news";
    ...
}