Build your own PHP Docker image

Knowing that the official Docker images for PHP work just fine for most use cases, you might think “Why would I do that?” — and that’s exactly what I thought until a few weeks ago when I had to add Kafka support to the php:7.1 and php:7.1-alpine images.

Note: You can skip the build up and jump to the actual implementation steps by clicking here. If you are just interested in easily extensible and small PHP base images, head over to kreait/php on Docker Hub or the corresponding GitHub repository.

After having finished the task, I realized two things:

  • We had just pulled in quite a lot of build dependencies to be able to compile a PHP extension, and had to clean up afterwards.
  • The official docker image itself contains a compiled from scratch version of PHP, and although the docker-ext-* helpers free us from a lot of manual work, we are still compiling the additional extensions that we might need.

Please notice the frequent use of the word “compile” :).

Example: Extending the PHP image to add the GD extension

Here is how you would add the GD extension to a custom PHP image (adapted from the README of the official PHP image):

FROM php:7.1
RUN apt-get update && apt-get install -y \
libfreetype6-dev \
libjpeg62-turbo-dev \
libpng12-dev \
&& docker-php-ext-configure gd \
--with-freetype-dir=/usr/include/ \
--with-jpeg-dir=/usr/include/ \
&& docker-php-ext-install -j$(nproc) gd

Which works fine:

docker build -t php:7.1-with-gd -f Dockerfile.debian .
#
docker images --filter "reference=php" --format "table {{.Repository}}\t{{.Tag}}\t{{.Size}}"
REPOSITORY TAG SIZE
php 7.1-with-gd 390MB
php 7.1 370MB
docker run php:7.1-with-gd sh -c "php -m | grep gd"
gd

But here are a couple of issues with this approach:

  • We have to figure out the equivalent compilation steps to add the GD extension when we wanted to extend the php:7.1-alpine image.
  • If we wanted to add other extensions, we would have to pull in other build dependencies.
  • Imagine we in a “normal” server environment, would we actually compile PHP, or would we use a system package?

Taking a step back: What are our requirements?

I imagine this (hopefully) reasonable list of requirements to our target PHP Docker image:

  1. It should be as readable as possible
    We can achieve this by using system packages instead of compiling PHP and additional extensions.
  2. It should be easy to extend
    We can achieve this by adding additional system packages.
  3. It should have a reasonable size
    We can achieve this by using the an Alpine based image instead of a Debian based one.

Choosing a base image

The official PHP images are currently based on debian:jessie and alpine:3.4 — both of which are not the latest available stable operating systems.

The reason (I assume) why we developers like Debian based systems is that we know the apt-get commands by heart: apt-get update -y && apt-get install -y <packages> is easy to write, but results in quite large image sizes (which would violate the third requirement).

Installing packages on Alpine Linux is as easy: apk --no-cache add <packages>, and as the latest Alpine release currently is 3.6, we’re going with it:

FROM alpine:3.6

Determining the packages we need

Let’s see which modules and extensions are provided out of the box with the official PHP image:

docker run php:7.1 sh -c “php -m”
[PHP Modules]
Core
ctype
curl
date
dom
fileinfo
filter
ftp
hash
iconv
json
libxml
mbstring
mysqlnd
openssl
pcre
PDO
pdo_sqlite
Phar
posix
readline
Reflection
session
SimpleXML
SPL
sqlite3
standard
tokenizer
xml
xmlreader
xmlwriter
zlib
[Zend Modules]

We know from the Dockerfile that PEAR/PECL is included as well. As Alpine Linux is very modular, we will need the following to provide the same set with our image:

FROM alpine:3.6
RUN apk --no-cache add \
php7 \
php7-ctype \
php7-curl \
php7-dom \
php7-fileinfo \
php7-ftp \
php7-iconv \
php7-json \
php7-mbstring \
php7-mysqlnd \
php7-openssl \
php7-pdo \
php7-pdo_sqlite \
php7-pear \
php7-phar \
php7-posix \
php7-session \
php7-simplexml \
php7-sqlite3 \
php7-tokenizer \
php7-xml \
php7-xmlreader \
php7-xmlwriter \
php7-zlib

It looks like much, but it is easy to read and understand (which satisfies our first requirement).

Lastly, as the most common use case is to use PHP with a webserver, the official image also adds a www-data user and group, which we just copy and paste from the official Dockerfile:

RUN set -x \
&& addgroup -g 82 -S www-data \
&& adduser -u 82 -D -S -G www-data www-data

To fully replicate the functionality of the offical image, we also want to provide the same entrypoint and make php -a the default command, which results in the following final Dockerfile :

FROM alpine:3.6
RUN apk --no-cache add \
php7 \
php7-ctype \
php7-curl \
php7-dom \
php7-fileinfo \
php7-ftp \
php7-iconv \
php7-json \
php7-mbstring \
php7-mysqlnd \
php7-openssl \
php7-pdo \
php7-pdo_sqlite \
php7-pear \
php7-phar \
php7-posix \
php7-session \
php7-simplexml \
php7-sqlite3 \
php7-tokenizer \
php7-xml \
php7-xmlreader \
php7-xmlwriter \
php7-zlib
RUN set -x \
&& addgroup -g 82 -S www-data \
&& adduser -u 82 -D -S -G www-data www-data
COPY entrypoint.sh /usr/local/bin/
ENTRYPOINT ["entrypoint.sh"]
CMD ["php", "-a"]

And that’s all! If we build the image and compare it to the official images, we can see that our image is even smaller while having the exact same features (which more than satisfies our third requirement):

docker build -t php:7.1-alpine-custom -f Dockerfile.alpine .
docker images --filter "reference=php" --format "table {{.Repository}}\t{{.Tag}}\t{{.Size}}"
REPOSITORY TAG SIZE
php 7.1-alpine-custom 22.9MB
php 7.1-alpine 56.8MB
docker run php:7.1-alpine-custom sh -c "php -m"
[PHP Modules]
Core
ctype
curl
date
dom
fileinfo
filter
ftp
hash
iconv
json
libxml
mbstring
mysqlnd
openssl
pcre
PDO
pdo_sqlite
Phar
posix
readline
Reflection
session
SimpleXML
SPL
sqlite3
standard
tokenizer
xml
xmlreader
xmlwriter
zlib
[Zend Modules]

Adding the GD extension

It is now very easy to extend our new base image to add the GD extension

FROM php:7.1-alpine-custom
RUN apk --no-cache add php7-gd

Which satisfies our second requirement and gives us the following result:

docker build -t php:7.1-alpine-with-gd -f Dockerfile.gd.alpine .
docker images --filter "reference=php" --format "table {{.Repository}}\t{{.Tag}}\t{{.Size}}"
REPOSITORY TAG SIZE
php 7.1-alpine-with-gd 29.8MB
php 7.1-alpine-custom 22.9MB
php 7.1-alpine 56.8MB

Conclusion

Using official Docker images is always a good choice, as they have proven stable (the official PHP Docker image has been pulled more than 10 million times) and are created by the creators who know best.

On the other hand, building your own PHP base image(s) is not hard and can save you time and effort in the long run, especially if you need to extend them for your specific requirements.

If you want to use and extend the results from the described steps, please feel free to head over to kreait/php on DockerHub and the corresponding GitHub Repository at https://github.com/kreait/docker-images/tree/master/php.

My next article will describe a way to create a one size fits all composer image based on our new base image which can be used for any docker-based PHP project and version.

One clap, two clap, three clap, forty?

By clapping more or less, you can signal to us which stories really stand out.