Close

Adding a Dockerized Service Dependency: PHP-FPM

A project log for Dockerize All the Things

hard drive crash! recover, revive, and re-engineer server using docker contained services this time.

ziggurat29ziggurat29 11/13/2020 at 18:060 Comments

Summary

Here we get a little bit more fancy by adding another microservice to the group:  this one for handling PHP processing.  We explore some things about networking in docker.

Deets

I have at least one PHP application on my personal web site, so I need PHP processing capability.  In the prior days of Apache2, that involved installing and configuring mod_php, but I am using nginx now, and apparently the way that is done is with PHP-FPM.  FPM (FastCGI Process Manager) is an alternative PHP FastCGI implementation.

One option is to derive a new docker image from the existing nginx one, and install PHP-FPM in it, alongside nginx.  However in this case I am going to use a curated PHP-FPM image from Dockerhub, and let the two containers cooperate.  This saves me the trouble of building/installing the PHP -- I should just have to configure it.  There is an 'official' image maintained by the PHP people that is for this machine architecture ('ARM64') and for Alpine Linux:  'php:fpm-alpine'

But first, a little bit about networking in Docker.

A Little Bit About Networking in Docker

Docker creates a virtual network for the various containers it runs.  There is a default one named 'default', however there is a quirk with it on Linux.  The machines (containers) on it do not have names, so it is a hassle to refer to other services.  If, however, you create a named network, then magically those machines (containers), will have DNS names, and they happen to be the name of the container.

There are several networking technologies you can use in Docker -- this is provided by what Docker calls a 'driver' -- and the most common technology for stuff like we are doing is to use a network bridge.  You may need to ensure you have bridge-utils installed first:

sudo apt install bridge-utils

Then we can create a named network that we will have Docker place its containers on:

docker network create -d bridge localnet

This network definition is persistent, so you can destroy it later when you're done with it:

docker network rm localnet

OK!  Now our containers will be able to communicate using hostnames.

PHP, Der' It Is!

The PHP-FPM has its own set of configuration files.  The curated Docker image 'php:fpm-alpine' has sane defaults for our need, but there is some sand in that Vaseline:  filesystem permissions on mounted directories/files.  These docker containers are running their own Linux installation, and so user ids and group ids are completely separate from those on the host filesystem.  For example, all my 'datadrive' files are usually owned by me, with UID:GID of 1001:1001.  However, this user does not exist in the PHP container.  The process there is running as 'www-data:www-data' (I don't know the numbers).  

There's several ways of dealing with this, but I chose to simply alter the config file that specifies the uid:gid to be 1001:1001.  The relevant file is located at '/usr/local/etc/php-fpm.d/www.conf', so I need to alter that.  Much like with nginx, I create a directory on datadrive that will hold my configuration overrides for the PHP stuff, and then mount that file into the container (thereby overriding what's already there).  First, I make my directory for that:

mkdir -p /mnt/datadrive/srv/php

OK, and here's a little trick:  if you mount a file or directory that does not exist on the host, then the first time you start the container, docker will copy the file/directory back onto the host.  This only happens if it's not there already.  It's really a bit lazy, but interesting to know.  Other mechanisms are good old fashioned cat, copy-paste, and there are also docker commands to explicitly extract files from images.  So, for the hacky way:

docker run --rm -it \
  --mount 'type=bind,src=/mnt/datadrive/srv/php/www.conf,dst=/usr/local/etc/php-fpm.d/www.conf' \
  php:fpm-alpine sh

and then you can immediately exit, thereby terminating and removing the container.  You should have seen that '/mnt/datadrive/srv/php/www.conf' has been magically created on your host system.  Edit it; around like 20-ish, make this happen:

...
; Unix user/group of processes
; Note: The user is mandatory. If the group is not set, the default user's group
;       will be used.
;user = www-data
user = 1001
;group = www-data
group = 1001
...

Now PHP in the container will be able to access those files as if it were you on the host system.  Obviously, set the UID:GID to what yours actually are.

So now we need to tell NGinx about how to handle PHP requests.  We need to modify /mnt/datadrive/srv/nginx/default.conf

server {
    listen       80;
    listen  [::]:80;
    server_name example.com www.example.com;

    #charset koi8-r;
    #access_log  /var/log/nginx/host.access.log  main;

    root /srv/www/vhosts/example.com;
    index  index.html index.htm;

    location / {
    }

    #error_page  404              /404.html;

    # redirect server error pages to the static page /50x.html
    #
    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }

    # pass the PHP scripts to FastCGI server listening on (docker network):9000
    #
    location ~ \.php$ {
        try_files $uri = 404; 
        fastcgi_split_path_info ^(.+\.php)(/.+)$; 
        fastcgi_pass php:9000; 
        fastcgi_index index.php; 
        include fastcgi_params; 
        fastcgi_param REQUEST_URI $request_uri; 
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; 
        fastcgi_param PATH_INFO $fastcgi_path_info; 
    }
}

So here we have added a section that basically says:  'for objects whose name ends in ".php", process them with the FastCGI interface to a system named 'php' on port 9000'.  And a few other incantations.  Especially note the 'fastcgi_pass php:9000', because the 'php' there is the machine name of our PHP_FPM container!

Testing

OK, now we are ready to test the two containers working together on the 'localnet' pseudo-network (don't forget to stop any other nginx containers that maybe you have running from other experiments):

docker run --rm -d -p 9000:9000 --name php \
    --network localnet \
    --mount 'type=bind,src=/mnt/datadrive/srv/php/www.conf,dst=/usr/local/etc/php-fpm.d/www.conf' \
    --mount 'type=bind,src=/mnt/datadrive/srv/www,dst=/srv/www' \
    php:fpm-alpine
docker run -it --rm -d -p 80:80 --name www \
    --network localnet \
    --mount 'type=bind,src=/mnt/datadrive/srv/www,dst=/srv/www' \
    --mount 'type=bind,src=/mnt/datadrive/srv/nginx/default.conf,dst=/etc/nginx/conf.d/default.conf' \
    nginx:alpine

In the root of my website is the venerable info.php:

<html>
<head>
<title>Info</title>
</head>
<body>
<p>
<?php
echo phpinfo();
?>
</p>
</body>
</html>

And now if I visit http://www.example.com/info.php, I should see all the infos!  The nginx noticed the file was a php kind, and asked the php-fpm system to process it for it!  Over the pseudo-network created by docker!

If for some reason things do not seem to be working, you can use

docker logs www
docker logs php

and the -f option can be used to follow.  Docker provides aggregated logging from the container by catching stdout/stderr, so these containers are often set up with files in /var/log symlinked to /dev/stdout and /dev/stderr.  This way you can see the logs easily on the host system, and also filter by container.

When you're done with testing, you can delete the docker network we created:

docker network rm localnet

OK, so now that is working, we need to get it working with systemd.  I could make another system service like I showed yesterday, but instead I'm going to use a tool called 'docker-compose' to specify a group of related docker images that need to be run together.  It also takes care of creating the network we will be needing.

Next

using docker-compose, and also with systemd

Discussions