pabis.eu

How to share files from PHP Docker image to Nginx

21 June 2023

If we are given an image with packed PHP application and it also contains resources like images, plain HTML files, we would like to serve them directly, without involvement of FPM. On a single instance, whether it is a container or a VM, it is easy - we just point both of them into the same directory. But what if we have two separate containers and we want to avoid building both with all the files included and keep the application files as part of the versioned image?

Repository for this post available here: https://github.com/ppabis/DockerShareVolume

To clarify this example, we will have the following Dockerfile for PHP image.

FROM php:7.4-fpm-alpine

COPY --chown=www-data:www-data index.php /srv/
COPY --chown=www-data:www-data assets /srv/

WORKDIR /srv

This image definition is simple, we just copy index.php and some assets to the container. Let's write some test script and add some images to the assets directory. This simple script will just scan the files in the assets and show them as images in HTML.

<!DOCTYPE html>
<HTML>
<HEAD>
    <META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=utf-8" />
</HEAD>
<BODY>
<?php
    $files = scandir('assets');
    foreach ($files as $file) {
        if (is_file('assets/' . $file)) {
            echo '<h3>' . $file . '</h3>';
            echo '<img src="assets/' . $file . '" />';
        }
    }
?>
</BODY>
</HTML>

Create assets directory, put some images there and build this container. Run with a plain Nginx configuration that just forwards to CGI.

server {
    listen 80;
    server_name _;

    root /srv;

    location ~ .* {
        fastcgi_pass php:9000;
        fastcgi_index index.php;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    }
}

Using Docker compose it will look like this.

version: '3.3'

networks:
  phpnginx:
    driver: bridge

services:
  php:
    build: .
    networks:
      - phpnginx

  nginx:
    image: nginx:latest
    volumes:
      - ./nginx.conf:/etc/nginx/conf.d/default.conf
    ports:
      - 8080:80
    networks:
      - phpnginx
    depends_on:
      - php

So run docker-compose up -d and try localhost:8080. After trying to load the page, we will see the list of files generated but the <img> will be broken.

Broken images

In the PHP container logs we can see:

2023-06-16 16:07:38 NOTICE: Access to the script
'/srv/assets/components-of-kubernetes.svg' has been denied (see
security.limit_extensions)

Let us try changing the value in PHP configuration. Add this line to Dockerfile

RUN sed -i 's/;security\.limit_extensions.*/security.limit_extensions = .php .png/g' /usr/local/etc/php-fpm.d/www.conf

Run docker-compose down, rebuild the image (docker-compose build) and try again. Now we will see that one .png image is showing up but the other is not.

One image

Warning: Unexpected character in input: '' (ASCII=127) state=0 in
/srv/assets/muilti-datacenter.png on line 131

It seems that this method is not promising. It would also be considered totally insecure if we were planning to let people upload their own images. We should use Nginx to serve static files. It will also be more performant then sending back and forth between PHP and Nginx containers. However, if some static files are packed inside the container we need to create a shared volume that will also copy the files from the PHP image first. In Docker compose we can do it like this.

...

volumes:
  app:

services:
  php:
    build: .
    volumes: # Important part
      - type: volume
        source: app
        target: /srv
        volume:
          nocopy: false
    ...

  nginx:
    image: nginx:latest
    volumes:
      - ./nginx.conf:/etc/nginx/conf.d/default.conf
      - app:/srv:ro
    ...

First we have to declare a Docker volume, without any host mount point. Next we add the volume to the PHP container. The nocopy set to false option will copy the files from PHP container into the volume. Then we mount the same volume in Nginx container under the same path. The ro option will make it read-only.

And in nginx.conf we would need to add a new location for static files. Files ending with .php will be forwarded to FPM and the rest will be served by Nginx.

    location ~ .*\.php$ {
        fastcgi_pass php:9000;
        fastcgi_index index.php;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    }

    location / {
        try_files $uri $uri/ =404;
    }

This time the images will be loaded correctly. We can also see that the files were served by Nginx and FPM only served index.php.

Images served correctly

# FPM
2023-06-20 21:38:48 192.168.32.3 -  20/Jun/2023:19:38:48 +0000 "GET /index.php" 200
# Nginx
2023-06-20 21:39:32 192.168.32.1 - - [20/Jun/2023:19:39:32 +0000] "GET /index.php HTTP/1.1" 200 353 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.5 Safari/605.1.15" "-"
2023-06-20 21:39:32 192.168.32.1 - - [20/Jun/2023:19:39:32 +0000] "GET /assets/components-of-kubernetes.png HTTP/1.1" 200 61595 "http://localhost:8080/index.php" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.5 Safari/605.1.15" "-"
2023-06-20 21:39:32 192.168.32.1 - - [20/Jun/2023:19:39:32 +0000] "GET /assets/multi-datacenter.png HTTP/1.1" 200 35680 "http://localhost:8080/index.php" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.5 Safari/605.1.15" "-"
2023-06-20 21:39:32 192.168.32.1 - - [20/Jun/2023:19:39:32 +0000] "GET /assets/docker.png HTTP/1.1" 200 4570 "http://localhost:8080/index.php" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.5 Safari/605.1.15" "-"

That way we can avoid splitting the project into two distributions of backend and frontend but pack the release into a single container and use Nginx to serve what should be static. When changing the image, remember to also remove the volume created by Docker Compose docker-compose down -v, otherwise the changes in PHP image will not be copied to the volume.