Inside Docker: One Nginx But Different PHP Versions Based on Your Hostname

A practical use case for the nginx http map module

Micha(el) Bladowski
Better Programming

--

Photo by Jon Tyson on Unsplash

Okay, here’s an everyday scenario everybody should know:

You have a typical docker-compose.yml setup with

  • nginx
  • php-fpm (PHP 7)
  • a database (not important for us here)

Let’s say your current PHP Version is 7.4 and you want to upgrade your application to PHP 8.1 and just extend your docker-compose.yml instead of just doing a “copy and paste” and duplicating everything. The goal is to keep both versions running at the same time, not replacing the existing config.

There are tons of ways to do this and all might be okay. With this posting, I just want to show you a way even I didn’t know after all that years.

So your default setup might be completely different, so I want to show you mine when it comes to PHP:

Nothing special, just a simple nginx/php-fpm combo.
The host.conf:

server {
listen 80;
server_name myapp.com;
root /app/public;
include /etc/nginx/php.conf;
}

The php.conf (just the important part):

location ~ \.php$ {
try_files $uri =404;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass php-fpm7:9000;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
}

So far, so good, nothing special here. So let’s say we want to add our new app and keep all that config files and reduce the duplication here. I stumbled over a nice feature of nginx: The ngx_http_map_module — after all that years, I didn’t know that it exists — till now :)

Let me show you how I am using this module in this special case:

Inside the nginx.conf in the “http” section, I added this:

map $http_host $phpfpm {
'myapp.com' 'php-fpm7'; <--- /app
'new.myapp.com' 'php-fpm8'; <--- /app-new
}

I extend my “host.conf” with the new virtual host:

server {
listen 80;
server_name new.myapp.com
root /app-new/public; <----- different folder !
include /etc/nginx/php.conf; <--- same config
}

and, as we remember, in our php.conf we have a fixed “fastcgi_pass”:

php-fpm7:9000

because with our new mapping, we are now able to replace this with our new $variable, so the line gets changed to:

$phpfpm:9000

But — something I had to learn as well with this — because nginx is running inside Docker, we have to tell nginx, which resolver nginx should use to be able to get the correct IP from our mapping hostnames: php-fpm7 and php-fpm8, so that’s why we have to add this:

resolver 127.0.0.11 ipv6=off;

and here’s the final result of our php.conf:

location ~ \.php$ {
resolver 127.0.0.11 ipv6=off;
try_files $uri =404;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass $phpfpm:9000;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
}

With this setup, nginx knows which PHP-Container we want to use based on our domain name, cool ;)

Of course, we have to extend our docker-compose.yml:

At the “nginx” service we have to mount the directory of your “app-new”:

- /home/app-new:/app-new

And we add another PHP service with the same mapping and
name it php-fpm8.

The end result looks like this:

Again, many solutions come to the same result, but I personally wanted to see how this “mapping” works and how to use it, I am happy with that solution, because:

  • I can re-use my existing main config: php.conf
  • I can use different PHP Versions in parallel, with the same APP or with another version of my app
  • I “could” use my php.conf for every project, not the best idea, but possible, because now the “mapping” tells nginx which container to use

So of course, if you want to test your app with different PHP Versions at the same time, your mapping could look like this:

map $http_host $phpfpm {
'myapp.com' 'php-fpm5';
'php7.myapp.com' 'php-fpm7';
'php80.myapp.com' 'php-fpm80';
'php81.myapp.com' 'php-fpm81';
}

It’s just an example, you get the idea ;)

For me, it’s a nice and clean way to handle different PHP Versions inside the same App.

Tip: Put the mapping into a separate config, in our example, let’s create
a php_mapping.conf and put the mapping inside (don’t forget to mount it in your nginx service) and replace the mapping inside the “http” section of our nginx.conf with:

include /etc/nginx/php_mapping.conf;

With that, even our nginx.conf has become reusable for any other project!

As always, I hope you could find any useful information with that posting

--

--