Nginx Location Priority and Fallback Routing Configuration for Multiple Applications


4 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";
    ...
}