Nginx: Using Regex to Configure Dynamic Location Blocks

Nginx: Using Regex to Configure Dynamic Location Blocks

I love Nginx as a webserver, it's quick and easy to configure (if you know what you are doing). As a web developer (or should I say programmer), I like to make things as easy as possible in my development environment. As my projects have increased over time I have noticed my Nginx config growing to accommodate each project.

My project directory looks something like:

  • root
    • project-1
      • public
        • index.php
    • project-2
      • public_html
        • index.php
    • project-3
      • public
        • index.php

And this goes on for a lots more projects using the same directory structure.

Now my Nginx conf file for this looks like , as all my projects use MVC all requests are routed through 'index.php'

server {
    listen 80;

    root /home/shaun/root;
    index index.html index.php;

    # Make site accessible from http://localhost/
    server_name localhost;
    client_max_body_size 100M;

    location / {
        try_files $uri $uri/ /index.php$is_args$args;
        autoindex on;
    }

    location /site-1/public {
        try_files $uri $uri/ /site-1/public/index.php?$is_args$args;
    }

    location /site-2/public_html {
        try_files $uri $uri/ /site-2/public_html/index.php?$is_args$args;
    }

    location /site-3/public {
        try_files $uri $uri/ /site-3/public/index.php?$is_args$args;
    }
    
    location  ~ \.(php)$ {
        proxy_buffer_size 128k;
        proxy_buffers 4 128k;
 
        fastcgi_keep_conn on;
        fastcgi_buffers 16 128k; 
        fastcgi_buffer_size 128k;
 
        fastcgi_pass   unix:/usr/local/php7.1/var/run/php-fpm.sock;
        fastcgi_read_timeout 1200;
        fastcgi_index  index.php;
        fastcgi_param  SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include        fastcgi_params;
    }
}

As you can see each site under root gets its own location block as if I didn't do this this then then a url like 'http://localhost/site-1/public/blog/list' would result in a rude default Nginx 404 'file not found' page instead of my sites blog list! You can see problems with this approach

  1. Each new site I have to add a new location block and restart the server
  2. Each time I delete a project I have to delete the location block and restart the server

Well I didn't want to keep adding/deleting sites from the config file. So what could I do? Looking over the location blocks and directory structure I saw a pattern and wondered if I could abstract all sites into one location block. This is what I came up with using regex for the location parameter.

First I put my php-fpm config in an include file in /etc/nginx

proxy_buffer_size 128k;
proxy_buffers 4 128k;

fastcgi_keep_conn on;
fastcgi_buffers 16 128k;
fastcgi_buffer_size 128k;

fastcgi_read_timeout 1200;
fastcgi_index  index.php;
fastcgi_param  SCRIPT_FILENAME $document_root$fastcgi_script_name;
include        fastcgi_params;

Then I rewrote my site config file in '/etc/nginx/sites-enabled/projects.conf'

server {
    listen 80;

    root /home/shaun/Web-Projects;
    index index.html index.php;

    # Make site accessible from http://localhost/
    server_name localhost;
    client_max_body_size 100M;

    location / {
        try_files $uri $uri/ /index.html;
        autoindex on;
    }

    location ~ /(.*)/(public_html|public) {

        try_files $uri $uri/ /$1/$2/index.php$is_args$args;

        location ~ \.(php)$ {
            include        php-fpm.conf;
            fastcgi_pass   unix:/usr/local/php7.1/var/run/php-fpm.sock;
        }
    }

    location ~ \.(php)$ {
        include        php-fpm.conf;
        fastcgi_pass   unix:/usr/local/php7.1/var/run/php-fpm.sock;
    }
}

Restart the server and now all my projects work that use this directory structure. Breaking down the new location block at lines 16-25

line 16 is the regex pattern ~ /(.*)/(public_html|public) This pattern has two groups, the first group matches any character after '/' which will match '/site-1', /'site-2/sub-site' etc. The next group matches the directory 'public' or 'public_html' after the next '/' so will match '/site-1/public', '/site-2/public_html' etc.

line 18 is the try_file directive which will check if the file or directory exists id so will server them if none of these is found it will server the 'index.php' defined by the regex groups as an example: '/<first group>/<second group>/index.php'.

I can now add and delete project site without touching the nginx config or restarting the server! :-)

Multiple PHP Config

Wait on a minute, after this I started wondering if I could run different PHP versions just by altering the URL to a prefix like '/php/7.0', /php/7/1' or '/php/7.2'. So with my new found knowledge of regex I set too.

Now my PHP versions are in separate directories:

/usr/local/php7.0
/usr/local/php7.1
/usr/local/php7.2

Yet another pattern we can use with regex so I come up with additional location block look like

....

location ~ ^/php/(7.0|7.1|7.2)(/.*/(public_html|public))? {

    try_files $uri $uri/ /php/$1$2/index.php$is_args$args;

    location ~ /php/(7.0|7.1|7.2).*\.(php)$ {
        include        php-fpm.conf;
        fastcgi_pass   unix:/usr/local/php$1/var/run/php-fpm.sock;
    }
}

....

Here I am using a new pattern to check if the URI starts with '/php/' then check the first group '(7.0|7.1|7.2)' this group matches which version I want then second and third group is the same as above but is optional and in the try_files directive I alter the directory structure to the matched pattern.

In the sub location block I again use regex to capture the PHP version number I want to run and use the matched pattern to the fastcgi_pass directive.

I restarted the server and tested my new block and YES! it worked now when I use the PHP prefixes in my URI I run that PHP version as you can see from the screen capture blow

php-7.0.pngphp7.1.pngphp7.2.png

So Now my Nginx confix file looks like

server {
    listen 80;

    root /home/shaun/Web-Projects;
    index index.html index.php;

    # Make site accessible from http://localhost/
    server_name localhost;
    client_max_body_size 100M;

    location / {
        try_files $uri $uri/ index.php$is_args$args;
        autoindex on;
    }

    location ~ ^/php/(7.0|7.1|7.2)(/.*/(public_html|public))? {

        try_files $uri $uri/ /php/$1$2/index.php$is_args$args;
        autoindex on;

        location ~ /php/(7.0|7.1|7.2).*\.(php)$ {
            include        php-fpm.conf;
            fastcgi_pass   unix:/usr/local/php$1/var/run/php-fpm.sock;
        }
    }

    location ~ /(.*)/(public_html|public) {

        try_files $uri $uri/ /$1/$2/index.php$is_args$args;

        location ~ \.(php)$ {
            include        php-fpm.conf;
            fastcgi_pass   unix:/usr/local/php7.1/var/run/php-fpm.sock;
        }
    }

    location ~ \.(php)$ {
        include        php-fpm.conf;
        fastcgi_pass   unix:/usr/local/php7.1/var/run/php-fpm.sock;
    }
}

A lot shorter and zero maintenance plus the ability to see all my sites in different PHP versions all at once just by changing the URL :-) Super!

Hope this helps and I would love to hear your comments on this and any improvements or different applications of this I would love to hear too.

See ya soon and happy coding!


24/09/2017 13:39:00 Shaun Freeman Filed Under: Linux Nginx, PHP, php-fpm, Server

Twitter Feed
Shaun Freeman @Zendmaster

Shaun Freeman @Zendmaster

I liked a @YouTube video https://t.co/shwHqVAisr PHP UK Conference 2016 - Marco Pivetta - Doctrine ORM Good Practices and Tricks

Shaun Freeman @Zendmaster

Working on new twitter updates for Uthando CMS.

Shaun Freeman @Zendmaster

I liked a @YouTube video https://t.co/RnEYvUDFx2 The Josephus Problem - Numberphile

Shaun Freeman @Zendmaster

I liked a @YouTube video https://t.co/TEGV4DhLGI Blown Like the Wind

Shaun Freeman @Zendmaster

I liked a @YouTube video https://t.co/lSFWmpHTX1 Patrick Stewart talks about meeting Sting on the set of DUNE (Funny to the EXTREME)