Nginx multiple location issues

I'm currently trying to break apart 3 applications from one repository into 3, but keeping the url structure, so basically different locations under the same domain have to be delivered by different applications.

What I'm struggling with is that one of the apps needs to be the fallback for non-existent urls, so if the first one doesn't match, and the second one doesn't, then the third should handle the request

The structure I've got is:

/etc/nginx/sites-enabled/main_site, in here, apart from server_name and logs I've got include /etc/nginx/subsites-enabled/*, where I've got 3 configuration files, one for each of the apps.

Each of the 3 config files contains a location block.

I've tried negative lookahead in regex (basically trying to hardcode the urls the other apps handle) but failed.

So, to summarise:

/ and /community should be delivered by /etc/nginx/subsites-enabled/example.org/home (a few perl scripts)

/news should be delivered by /etc/nginx/subsites-enabled/example.org/news (wordpress)

everything else should be delivered by /etc/nginx/subsites-enabled/example.org/app (cake app)

The perl bit works fine. The problem I'm having is that the app is taking over news (probably because it matches .*), I've tried various options (I've been at this for 2 days) but none of them solved all the problems (sometimes static assets wouldn't work, etc).

My configuration is:

/etc/nginx/sites-enabled/example.org:

server {
    listen   80;
    server_name example.org;
    error_log /var/log/nginx/example.org.log;

    include /etc/nginx/subsites-enabled/example.org/*;
}

/etc/nginx/subsites-enabled/example.org/home:

location = / {
  rewrite ^.*$ /index.pl last;
}

location ~* /community(.*) {
  rewrite ^.*$ /index.pl last;
}

location ~ \.pl {
  root   /var/www/vhosts/home;
  access_log /var/log/nginx/home/access.log;
  error_log /var/log/nginx/home/error.log;

  include /etc/nginx/fastcgi_params;
  fastcgi_index index.pl;
  fastcgi_param SCRIPT_FILENAME /var/www/vhosts/home$fastcgi_script_name;
  fastcgi_pass  unix:/var/run/fcgiwrap.socket;
}

/etc/ngins/subsites-enabled/news

location /news {
  access_log /var/log/nginx/news/access.log;
  error_log /var/log/nginx/news/error.log debug;

  error_page 404 = /news/index.php;

  root /var/www/vhosts/news;

  index index.php;

  if (!-e $request_filename) {
      rewrite ^.*$ /index.php last;
  }

  location ~ \.php {
    include /etc/nginx/fastcgi_params;
    fastcgi_pass  127.0.0.1:9000;
    fastcgi_index index.php;
    fastcgi_param SCRIPT_FILENAME /var/www/vhosts/news$fastcgi_script_name;
  }
}

/etc/nginx/subsites-enabled/app:

location ~ .* {
  access_log /var/log/nginx/app/access.log;
  error_log /var/log/nginx/app/error.log;

  rewrite_log on;

  index index.php;
  root /var/www/vhosts/app/app/webroot;

  if (-f $request_filename) {
    expires 30d;
    break;
  }

  if (!-e $request_filename) {
    rewrite ^.*$ /index.php last;
  }

  location ~ \.php {
    include /etc/nginx/fastcgi_params;
    fastcgi_pass  127.0.0.1:9000;
    fastcgi_index index.php;
    fastcgi_param SCRIPT_FILENAME /var/www/vhosts/app/app/webroot$fastcgi_script_name;
  }
}

Solution 1:

There are a few things wrong with your config, the two relevant ones being:

  1. Paths within a location block still include the matched path.
  2. Rewrites with 'last' continue by looking through all available locations for a match (they break out of the current location block).

For instance, take the URL example.org/news/test.htm

  • The location /news block will match it
  • The path used is then /news/test.htm - this does not change, just because it is in the location block
  • Adding the path to the document_root, you get: /var/www/vhosts/news/news/test.htm
  • Your if (!-e $request_filename) statement should capture this non-existent file
  • You rewrite the path to /index.php
  • Since you are using last the processes starts over (breaking out of the location block)
  • /index.php is now captured by the location /app block.

The problem mentioned above, with the root directive, is compounded when you go to your app location block. Unlike with the 'news' block, where you could conceivably just remove 'news' from the path (since it will be added back in), you cannot do this for the app path, which ends in 'webroot'.

The solution lies in the alias directive. This does not change the document_root, but it does change the file path that is used to serve the request. Unfortunately, rewrite and try_files tend to behave a bit unexpectedly with alias.

Let's start with a simple example - no PHP - just HTML and your Perl block - but with a folder structure matching yours (tested on Nginx 1.0.12, CentOS 6):

server {
    server_name example.org;
    error_log /var/log/nginx/example.org.error.log notice;
    access_log /var/log/nginx/example.org.access.log;
    rewrite_log on;

    location = / {
        rewrite ^ /index.pl last;
    }

    location ^~ /community {
        rewrite ^ /index.pl last;
    }

    location ~ \.pl {
        root   /var/www/vhosts/home;

        [fastcgi_stuff...]
    }


    location ^~ /news {
        alias /var/www/vhosts/news;
        index index.htm;

        try_files $uri $uri/ /news/index.htm;
    }

    location ^~ /app {
        alias /var/www/vhosts/app/app/webroot;
        index index.htm;

        try_files $uri $uri/ /app/index.htm;
    }

    location / {
        rewrite ^/(.*) /app/$1 last;
    }
}
  • location = / - will only match the root path
  • location ^~ /community - will match every path starting with /community
  • location ~ \.pl - will match all files that contain .pl
  • location ^~ /news - will match every path starting with /news
  • location ^~ /app - will match every path starting with /app
  • location / - will match all paths not matched above

You should be able to remove the ^~ - but it may offer a slight performance improvement, since it stops searching once a match is found.

While it should be a simple matter to add the PHP blocks back in, there is, unfortunately, a slight difficulty - try_files (and your rewrite) do not end up passing the desired path to the nested location block - and using alias when only the extension is specified in the location block doesn't work.

One solution is to use separate location blocks that perform a capture together with the alias directive - it isn't quite elegant, but as far as I can tell, it does work (again, tested on Nginx 1.0.12, CentOS 6 - of course, I didn't setup CakePHP, Wordpress, and Perl - I just used a couple of PHP and HTML files in each folder)

server {
    server_name example.org;
    error_log /var/log/nginx/example.org.error.log notice;
    access_log /var/log/nginx/example.org.access.log;
    rewrite_log on;

    location = / {
        rewrite ^ /index.pl last;
    }

    location ^~ /community {
        rewrite ^ /index.pl last;
    }

    location ~ \.pl {
        root   /var/www/vhosts/home;
        access_log /var/log/nginx/home.access.log;
        error_log /var/log/nginx/home.error.log;
        include /etc/nginx/fastcgi_params;
        fastcgi_index index.pl;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_pass  unix:/var/run/fcgiwrap.socket;
    }

    location /news {
        access_log /var/log/nginx/news.access.log;
        error_log /var/log/nginx/news.error.log notice;
        alias /var/www/vhosts/news;
        index index.php;
        try_files $uri $uri/ /news/index.php;
    }

    location ~* ^/news/(.*\.php)$ {
        access_log /var/log/nginx/news.php.access.log;
        error_log /var/log/nginx/news.php.error.log notice;
        alias /var/www/vhosts/news/$1;
        try_files "" /news/index.php;
        include /etc/nginx/fastcgi_params;
        fastcgi_index index.php;
        fastcgi_param SCRIPT_NAME $1;
        fastcgi_param SCRIPT_FILENAME /var/www/vhosts/news/$1;
        fastcgi_pass  127.0.0.1:9000;
    }

    location /app {
        alias /var/www/vhosts/app/app/webroot;
        access_log /var/log/nginx/app.access.log;
        error_log /var/log/nginx/app.error.log notice;
        index index.php;
        try_files $uri $uri/ /app/index.php;
    }

    location ~* ^/app/(.*\.php)$ {
        access_log /var/log/nginx/news.access.log;
        error_log /var/log/nginx/news.error.log notice;
        alias /var/www/vhosts/app/app/webroot/$1;
        try_files "" /app/index.php;
        include /etc/nginx/fastcgi_params;
        fastcgi_index index.php;
        fastcgi_param SCRIPT_NAME $1;
        fastcgi_param SCRIPT_FILENAME /var/www/vhosts/app/app/webroot/$1;
        fastcgi_pass  127.0.0.1:9000;
    }

    location / {
        rewrite ^/(.*) /app/$1 last;
    }
}

The above config, takes the simple one above, and makes two changes:

  • Add two location blocks:
    • location ~* ^/news/(.*\.php)$ - will match all files ending in .php, with paths starting with /news/
    • location ~* ^/app/(.*\.php)$ - will match all files ending in .php, with paths starting with /app/
  • Remove the ^~ matching - this is required so that the two added location blocks can match against paths (otherwise matching would stop on the /news or /app blocks).

It should be noted that the order for location matching is very important here:

  • Exact matches first (using =)
  • Matches with ^~ second
  • Matching regex blocks
  • Conventional strings - only if no matching regex is found

A matching regex will supersede a straight string!

An important point of mention is that when captures are used with alias, the entire URL is replaced - not just the leading folder. Unfortunately, this means that $fastcgi_script_name is left empty - so, I have used $1 above instead.

I am sure you will need to make a few changes, but the basic premise should be functional. You should be able to separate the blocks into multiple files as needed - ordering shouldn't affect the config.