PHP FPM with nginx under Docker

I’ve been migrating my personal site over to Docker. Everything is beautifully containerized now and I’m pretty happy with it. I wanted to make a quick blog post about a non-obvious error I had along the way.

The problem:

My Docker setup has separate containers for nginx and php-fpm, which is super convenient to set up. Writing the docker-compose.yml config took about 10 minutes tops, but more on that later… I was getting an odd error trying to run PHP scripts. In the browser, this manifested as a “File not found” error. Checking the server logs using `docker-compose logs -f nginx` I found:
nginx | 2018/09/01 18:10:54 [error] 5#5: *11 FastCGI sent in stderr: "Primary script unknown" while reading response header from upstream, client: 172.18.0.4, server: sjmf.in, request: "GET /hello.php HTTP/1.1", upstream: "fastcgi://172.18.0.2:9000", host: "sjmf.in"
… and in my php-fpm container, the log showed:
php-fpm | 172.18.0.4 - 01/Sep/2018:18:10:54 +0000 "GET /hello.php" 404
My php-fpm container has access to the same files as the nginx container, mounted read-only. It’s looking for hello.php, but not finding it. (172.18.0.4 is the resolved IP of my nginx container, and 172.18.0.2 is the php-fpm container.)

What it meant:

I tracked down the error via StackOverflow but it took a little while to properly understand what was going on. The core problem was that /hello.php was being looked up in a different root path than I was expecting. Here’s my original nginx config:
server {
    listen 80; 
    server_name sjmf.in;
    root /usr/share/nginx/html;

    location ~ [^/]\.php(/|$) { 
        fastcgi_split_path_info ^(.+?\.php)(/.*)$;
        if (!-f $document_root$fastcgi_script_name) {
            return 404;
        }

        fastcgi_pass php-fpm:9000;

        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_param PATH_INFO $fastcgi_path_info;
        fastcgi_param PATH_TRANSLATED $document_root$fastcgi_path_info;
    }
}
I’ve bolded the important bits of that block. The search path for files is set by the root directive– it points at /usr/share/nginx/html, which is where my files are in my nginx container. The variable $document_root then gets passed through to the php-fpm container so that it knows where to look for files too. But in the php-fpm container, the default is to look for files in /var/www/html… so that was of course where I’d mounted them in my docker-compose.yml (see full config below). So in the end, it turned out to be a simple path mismatch. “File not found” really did mean not found– because I hadn’t properly understood how php-fpm gets its root directory for reading files and assumed it was looking in its’ own working directory.

How I fixed it:

Following the logic of the linked StackOverflow post, my initial thought for fixing this was to pass through the DOCUMENT_ROOT for php-fpm manually, like so:
fastcgi_param  DOCUMENT_ROOT    /var/www/html;
fastcgi_param  SCRIPT_FILENAME  /var/www/html/$fastcgi_script_name;
But a more elegant way was possible by simply mounting my files volume in the same place on both containers. So I changed my docker-compose.yml to mount my web files volume into /var/www/html/:
volumes:
  - "./volumes/sjmf.in/:/var/www/html"
… and in the nginx config, updated my document root to:
root /var/www/html;
And now $document_root is the same for both the nginx and php-fpm containers. Here’s my final docker-compose.yml:
# Main website container (nginx)
nginx:
  image: nginx
  container_name: nginx
  restart: always
  volumes:
    - "./sjmf-in.conf/:/etc/nginx/conf.d"
    - "./volumes/sjmf.in/:/var/www/html"
# Old web files mount point
#   - "./volumes/sjmf.in/:/usr/share/nginx/html"

# PHP fpm CGI server
php-fpm:
  image: php:rc-fpm-alpine 
  container_name: php-fpm
  restart: always
  volumes:
    - "./volumes/sjmf.in:/var/www/html:ro
Note that I’ve also updated my volume mount point in the docker-compose.yml to match where I’ve got it in my nginx config.

Postscript and a shout-out:

I’ve not quite described my Docker setup fully in this post, because I’ve also got a container for mysql, a dedicated container for WordPress, and then everything is behind an nginx reverse proxy which handles the subdomains and SSL. I set up my SSL certs using Let’s Encrypt and the fantastic Docker + Nginx + Let’s Encrypt sample by @gilyes made it trivial (see also https://gilyes.com/docker-nginx-letsencrypt/ for tutorial). It was really easy to build on top of this example and docker-compose did all the heavy lifting. Highly recommended. 👍