David NĂ©grier CTO

We recently released the v2 of thecodingmachine/php docker images. It is a set of "general purpose" PHP images for Docker, with a big emphasize on developer experience.

TL;DR

Have a look at thecodingmachine/php. This project contains a set of Docker images containing PHP with:

  • 3 variants: Apache, CLI or PHP-FPM
  • NodeJS (optional, version 8 or 10)
  • The most common PHP extensions (use environment variables to enable them)
  • Cron (configurable via environment variables)
  • ... and much more!

What's new?

thecodingmachine/php v1 was a set of fat images. Fat? They contain almost all the PHP extensions available in PHP. This was very useful for a development environment, but of course, it's not optimal: the image ships with a number of PHP extensions you will never use.

thecodingmachine/php v2 brings new "slim" images. They are designed to be used as a base image for your production images. They come with no extensions pre-loaded, but you can enable any PHP extension with a single "ARG" in your Dockerfile.

Also, PHP 7.3 is now available in the supported versions.

v2 also comes with a number of new extensions, and in particular Swoole, php-ds and mailparse.

Image type Fat Slim (new)
Typical usage Development environment, CI Optimized production image
Extensions enabled by default apcu mysqli opcache pdo pdo_mysql redis zip soap none
Extensions available (compiled) Virtually all popular extensions none
Enabling an extension Use an environment variable. i.e.: PHP_EXTENSIONS="xdebug swoole" Add it at build time: ARG PHP_EXTENSIONS="xdebug swoole"

Fat images

The usage of "fat" images is unchanged. They are designed to be used in docker-compose.

docker-compose.yml

version: '3'
services:
  my_app:
    image: thecodingmachine/php:7.3-v2-apache-node10
    environment:
      # Enable the PostgreSQL extension
      PHP_EXTENSION_PGSQL: 1
      # Disable the Mysqli extension (otherwise it is enabled by default)
      PHP_EXTENSION_MYSQLI: 0

Slim images

Slim images are new. They are meant to be used as a base image in your Dockerfiles. Slim images come with no extension, but you can compile any of them at build time:

Dockerfile

ARG PHP_EXTENSIONS="apcu mysqli opcache pdo pdo_mysql redis zip soap"
FROM thecodingmachine/php:7.3-v2-slim-apache
# The build will automatically trigger the download and compilation
# of the extensions (thanks to a ONBUILD hook in the slim image)

The build process does not only take care of running pecl install. It does also install all the required dependencies and it takes care of removing the unneeded development files once the build is done.

But also...

thecodingmachine/php v2 images come with all the goodies from v1. It is a "batteries included" PHP image:

  • Composer is installed
  • nano editor is installed
  • the xdebug extension, if enabled, will configure the remote host automatically (and correctly whether you use Linux, Windows or MacOS!)
  • ...

Variants

Images are tagged according to PHP version, image version, variant and Node version.

For instance:

thecodingmachine/php:7.3-v1-[slim]-apache-node10
                      ^   ^    ^     ^       ^
                      |   |    |     |       |
             PHP version  |    |     |       |
                          |    |     |       |
                Image version  |     |       |
                               |     |       |
                         Slim / Fat  |       |
                                     |       |
               Variant (apache, cli or fpm)  |
                                             |
                     Node version (empty, 8 or 10)                   

We have images for PHP 7.1, 7.2 and PHP 7.3.

Extremely configurable

Using only environment variables you can:

  • Edit any php.ini setting
  • Add/remove PHP extensions
  • Add/remove Apache extensions (Apache variant only)
  • Add startup scripts (for instance "composer install")
  • Add Cron jobs

docker-compose.yml

version: '3'
services:
  my_app:
    image: thecodingmachine/php:7.3-v2-apache-node8
    environment:
      # Let's enable the PostgreSQL extension and disable the Mysqli extension!
      PHP_EXTENSION_PGSQL: 1
      PHP_EXTENSION_MYSQLI: 0
      # set the parameter memory_limit=1g
      PHP_INI_MEMORY_LIMIT: 1g
      # set the parameter error_reporting=EALL
      PHP_INI_ERROR_REPORTING: E_ALL
      # Let's enable Webdav and SSL on Apache
      APACHE_EXTENSION_DAV: 1
      APACHE_EXTENSION_SSL: 1
      # Apache document root is the "web" directory in your project.
      APACHE_DOCUMENT_ROOT: web/
      # On startup, let's run composer install and apply DB migrations
      STARTUP_COMMAND_1: composer install
      STARTUP_COMMAND_2: vendor/bin/doctrine orm:schema-tool:update
      # Finally, let's set-up cron jobs
      CRON_USER_1: root
      CRON_SCHEDULE_1: * * * * *
      CRON_COMMAND_1: vendor/bin/console do:stuff

Other changes

The structure of the project has been deeply modified to make it more manageable.

All extensions have now a dedicated installation script.

It is way easier to create a pull request for v2 than it was for v1.

Miscellaneous changes:

  • PHP 7.1 base image is now Debian Stretch
  • We dropped support for Node6. Node 8 and Node 10 are available.

Usage in continuous integration environments

Those images can be tremendously useful in continuous integration environments.

At TheCodingMachine, we are pretty fond of Gitlab CI. Our CI file now looks like this:

test:
  image: thecodingmachine/php:7.3-v2-cli
  variables:
    PHP_EXTENSIONS=gd event
  before_script:
    - composer install
  script:
    - vendor/bin/phpunit

Woot! So easy!

Actually, I can even replace vendor/bin/phpunit by phpunit alone, because ./vendor/bin is part of the PATH. That's right, anything in vendor/bin directory can be accessed from your project's root directory... Did I say developer friendly? :)

File permissions management

Permissions management is a tricky issue when it comes to Docker. Depending on your use case (development or production), and depending on the OS you are using, you can have a wide range of issues to solve. We really tried to do our best to simplify this, without sacrificing security.

File permissions on a development environment

In a development environment, you mount your working directory into the container's /var/www/html directory.

For a good development workflow:

  • your IDE must have write access to the files
  • your web-server should have write access to the files (for caching or upload purposes)
  • scripts executed in the container (like composer install or php-cs-fixer) should have write access too

If you are using MacOS or Windows, Docker does not really enforce any permissions in the file system. For instance, any user can modify any files owned by root on a OSX docker mount point.

For Linux users, things are more secure... and more tricky! Docker will enforce the permissions across the mount points. If your run a composer install in the Docker container as root, your files will belong to root on the host file system. This is something you want to avoid because your IDE won't be able to touch those files!

The thecodingmachine/php image solves this problem by taking the following steps:

  • Out of the box, it has a docker user (whose ID is 1000 by default)
  • Apache is run by this docker user (and not by www-data)
  • On container startup, a script will first try to detect if the /var/www/html directory is mounted or not, and whether it is a Windows, MacOS, or Linux mount.
  • If this is a Linux mount, it will look at the owner of the /var/www/html directory. Let's assume the directory belongs to the user "foobar" whose ID is 1001. Dynamically, the container will change the docker ID to be 1001 (instead of 1000) by default. This is done using the -u flag of the usermod command.
  • Therefore, the ID of the docker user (that is running Apache) and the ID of the mounted directory owner on the host are matching. No more permission issues while developing (Hooray!)

File permissions on a production environment

Of course, on a production environment, you don't want this. On a production environment, you will typically not use any mount. Instead, you will copy your PHP files inside the container's /var/www/html directory. By default, the /var/www/html directory belongs to www-data. The container will detect this and act accordingly.

You should still give back ownership of the Apache processes to the www-data user. This can be done easily using one more environment variable:

ENV APACHE_RUN_USER=www-data \
    APACHE_RUN_GROUP=www-data

Is this following Docker best practices?

thecodingmachine/php v1 was violating a number of Docker best-practices. In particular, it contained most of the PHP extensions, even if you did not use them. thecodingmachine/php v2 images with the slim variant are fixing this issue, so they are really better tailored for production usage.

Are they 100% state-of-the-art Docker images? Probably not.

So you want to be state-of-the-art?

Instead of using thecodingmachine/php, you should do this:

Avoid installing unnecessary packages

thecodingmachine/php v2 contains a lot of pretty useful packages for development, but that are not needed for production (like the nano editor, or Composer).

If you want to be state of the art, you should write your own Dockerfile and install the bare-minimum in the container. Of course, you should store the image in your own registry.

Use multi-stage builds

Some variants of thecodingmachine/php come with NodeJS installed. The expectation is that you will need NodeJS to build your JS/CSS assets (probably using webpack). But the image will run in production with NodeJS installed, while it is absolutely not necessary (it is used only at build time).

Starting with Docker 17.05, Docker added this wonderful feature named multi-stage builds.

From your Dockerfile, you can call another container to perform build stages. From your PHP container, you could call a NodeJS container to perform a build and copy the built files back to your PHP container. The PHP container never stores NodeJS runtime. Useful.

Each container should have only one concern

thecodingmachine/php images bundle cron. So strictly speaking, they have 2 concerns:

  • one is to answer HTTP requests
  • one is to trigger events at regular intervals

If you want to be state of the art, you should delegate the scheduling of events to a separate container like Tasker or one of the other alternatives.

Image size

Here are a few image size comparison:

Image Size (uncompressed) Variation
php:7.2-apache 377 MB
7.2-v2-slim-apache 460 MB +83 MB (+22%)
7.2-v2-apache 606 MB +229 MB (+60%)

So should I use the thecodingmachine/php images?

Give it a try, you won't regret it!

thecodingmachine/php general purpose images help you starting quickly, while ensuring a pretty decent quality.

About the author

David is CTO and co-founder of TheCodingMachine and WorkAdventure. He is the co-editor of PSR-11, the standard that provides interoperability between dependency injection containers. He is also the lead developper of GraphQLite, a framework-agnostic PHP library to implement a GraphQL API easily.