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. 👍