A lean, base Docker image For PHP

Emmanuel Merali
8 min readOct 19, 2023

--

Docker images can become incredibly bloated, particularly as they are progressively augmented during the development phase.

Specifically, when using PHP, one might naturally choose an official PHP image such as the excellent php:8.1-fpm-alpine or php:8.1-fpm images as a base for the project’s Dockerfile. These images are official Docker images so they are conscientiously maintained, kept up-to-date and can be trusted.

However, more often than not, these images lack essential extensions that will be needed to develop serious projects, such as redis or opcache.

Herein lies the problem: in order to add extensions to these base images, they have to be compiled, which means installing all the necessary development tools and libraries. Let’s have a look at an example:

FROM php:8.1-fpm

RUN apt-get update && \
apt-get install -y --no-install-recommends \
libzip-dev \
libz-dev \
libzip-dev \
libjpeg-dev \
libpng-dev \
libfreetype6-dev \
libssl-dev \
libxml2-dev \
libreadline-dev \
unzip \
supervisor

#####################################
# PHP Extensions
#####################################
# Install the PHP shared memory driver
RUN pecl install APCu && \
docker-php-ext-enable apcu

# Install the PHP bcmath extension
RUN docker-php-ext-install bcmath

# Install for image manipulation
RUN docker-php-ext-install exif

# Install the PHP graphics library
RUN docker-php-ext-configure gd \
--with-freetype \
--with-jpeg
RUN docker-php-ext-install gd

# Install the PHP intl extention
RUN docker-php-ext-install intl

# Install the PHP mysqli extention
RUN docker-php-ext-install mysqli && \
docker-php-ext-enable mysqli

# Install the PHP opcache extention
RUN docker-php-ext-install opcache

# Install the PHP pcntl extention
RUN docker-php-ext-install pcntl

# Install the PHP pdo_mysql extention
RUN docker-php-ext-install pdo_mysql

# Install the PHP redis driver
RUN pecl install redis && \
docker-php-ext-enable redis

# install XDebug but without enabling
RUN pecl install xdebug

# Install the PHP zip extention
RUN docker-php-ext-install zip

#####################################
# Composer
#####################################
RUN curl -s http://getcomposer.org/installer | php && \
mv composer.phar /usr/local/bin/composer

#####################################
# Entrypoint
#####################################
COPY ./docker/php-fpm/docker-entrypoint.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
RUN ln -s /usr/local/bin/docker-entrypoint.sh /

WORKDIR /var/www/html
COPY . /var/www/html/

RUN composer install

#####################################
# Cleanup
#####################################
RUN apt-get autoremove --purge -y \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*

ENTRYPOINT ["docker-entrypoint.sh"]
CMD ["docker-entrypoint.sh"]

The resulting image is about 900M, including the source code and probably a lot of things that are not needed. Can this Dockerfile be improved?

The answer is yes and the simple solution is to use a slimmer base image. Let’s try to use an alpine based image:

FROM php:8.1-fpm-alpine

RUN apk update --no-cache && \
apk upgrade --no-cache
RUN apk add --no-cache \
supervisor
RUN apk add --no-cache --virtual .build-deps \
$PHPIZE_DEPS \
linux-headers
RUN apk add --no-cache \
freetype-dev \
jpeg-dev \
icu-dev \
libzip-dev

#####################################
# PHP Extensions
#####################################
# Install the PHP shared memory driver
RUN pecl install APCu && \
docker-php-ext-enable apcu

# Install the PHP bcmath extension
RUN docker-php-ext-install bcmath

# Install for image manipulation
RUN docker-php-ext-install exif

# Install the PHP graphics library
RUN docker-php-ext-configure gd \
--with-freetype \
--with-jpeg
RUN docker-php-ext-install gd

# Install the PHP intl extention
RUN docker-php-ext-install intl

# Install the PHP mysqli extention
RUN docker-php-ext-install mysqli && \
docker-php-ext-enable mysqli

# Install the PHP opcache extention
RUN docker-php-ext-enable opcache

# Install the PHP pcntl extention
RUN docker-php-ext-install pcntl

# Install the PHP pdo_mysql extention
RUN docker-php-ext-install pdo_mysql

# Install the PHP redis driver
RUN pecl install redis && \
docker-php-ext-enable redis

# install XDebug but without enabling
RUN pecl install xdebug

# Install the PHP zip extention
RUN docker-php-ext-install zip

#####################################
# Composer
#####################################
RUN curl -s http://getcomposer.org/installer | php && \
mv composer.phar /usr/local/bin/composer

#####################################
# Entrypoint
#####################################
COPY ./docker/php-fpm/docker-entrypoint.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
RUN ln -s /usr/local/bin/docker-entrypoint.sh /

WORKDIR /var/www/html
COPY . /var/www/html/

RUN composer install

#####################################
# Cleanup
#####################################
RUN apk del --no-network .build-deps
RUN rm -rf /tmp/* /var/tmp/*

ENTRYPOINT ["docker-entrypoint.sh"]
CMD ["docker-entrypoint.sh"]

Not bad, the image is now just over 600M. Using alpine made a difference. But it’s still not satisfactory, as many unnecessary files are still taking space and bloat the image with no purpose. Can we still improve upon the use of a slimmer base image?

The answer to this question is also yes, by using Docker multi-stage builds. In a multi-stage build, each build stage is responsible for a specific part of the build process - such as building extensions or installing composer modules - but then is ditched together with its garbage in a later build stage. Only the relevant artifacts can be copied from one stage to the other, leaving behind the bloat.

With this in mind, the Dockerfile becomes as follows:

FROM php:8.1-fpm-alpine as base

RUN apk update --no-cache && \
apk upgrade --no-cache
RUN apk add --no-cache \
supervisor

FROM base as build

RUN apk add --no-cache \
$PHPIZE_DEPS \
linux-headers
RUN apk add --no-cache \
freetype-dev \
jpeg-dev \
icu-dev \
libzip-dev

#####################################
# PHP Extensions
#####################################
# Install the PHP shared memory driver
RUN pecl install APCu && \
docker-php-ext-enable apcu

# Install the PHP bcmath extension
RUN docker-php-ext-install bcmath

# Install for image manipulation
RUN docker-php-ext-install exif

# Install the PHP graphics library
RUN docker-php-ext-configure gd \
--with-freetype \
--with-jpeg
RUN docker-php-ext-install gd

# Install the PHP intl extention
RUN docker-php-ext-install intl

# Install the PHP mysqli extention
RUN docker-php-ext-install mysqli && \
docker-php-ext-enable mysqli

# Install the PHP opcache extention
RUN docker-php-ext-enable opcache

# Install the PHP pcntl extention
RUN docker-php-ext-install pcntl

# Install the PHP pdo_mysql extention
RUN docker-php-ext-install pdo_mysql

# Install the PHP redis driver
RUN pecl install redis && \
docker-php-ext-enable redis

# install XDebug but without enabling
RUN pecl install xdebug

# Install the PHP zip extention
RUN docker-php-ext-install zip

FROM base as target

#####################################
# Install necessary libraries
#####################################
RUN apk add --no-cache \
freetype \
jpeg \
icu \
libzip

#####################################
# Copy extensions from build stage
#####################################
COPY --from=build /usr/local/lib/php/extensions/no-debug-non-zts-20210902/* /usr/local/lib/php/extensions/no-debug-non-zts-20210902
COPY --from=build /usr/local/etc/php/conf.d/* /usr/local/etc/php/conf.d

#####################################
# Composer
#####################################
RUN curl -s http://getcomposer.org/installer | php && \
mv composer.phar /usr/local/bin/composer

#####################################
# Entrypoint
#####################################
COPY ./docker/php-fpm/docker-entrypoint.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
RUN ln -s /usr/local/bin/docker-entrypoint.sh /

WORKDIR /var/www/html
COPY . /var/www/html/

RUN composer install

ENTRYPOINT ["docker-entrypoint.sh"]
CMD ["docker-entrypoint.sh"]

All right, now the final image is just under 180M. That’s over 80% smaller than what we started with. There is one last step that we can take to improve the build speed this time, since the size is pretty well optimized. Just split the image in 2 and use the first one as the base for the second one. This way, we only need to do the hard work only once. After that, it’s just a question of creating a Docker image with our code in it.

Let’s do it this way:

# Base image
FROM php:8.1-fpm-alpine as base

RUN apk update --no-cache && \
apk upgrade --no-cache
RUN apk add --no-cache \
supervisor

FROM base as build

RUN apk add --no-cache \
$PHPIZE_DEPS \
linux-headers
RUN apk add --no-cache \
freetype-dev \
jpeg-dev \
icu-dev \
libzip-dev

#####################################
# PHP Extensions
#####################################
# Install the PHP shared memory driver
RUN pecl install APCu && \
docker-php-ext-enable apcu

# Install the PHP bcmath extension
RUN docker-php-ext-install bcmath

# Install for image manipulation
RUN docker-php-ext-install exif

# Install the PHP graphics library
RUN docker-php-ext-configure gd \
--with-freetype \
--with-jpeg
RUN docker-php-ext-install gd

# Install the PHP intl extention
RUN docker-php-ext-install intl

# Install the PHP mysqli extention
RUN docker-php-ext-install mysqli && \
docker-php-ext-enable mysqli

# Install the PHP opcache extention
RUN docker-php-ext-enable opcache

# Install the PHP pcntl extention
RUN docker-php-ext-install pcntl

# Install the PHP pdo_mysql extention
RUN docker-php-ext-install pdo_mysql

# Install the PHP redis driver
RUN pecl install redis && \
docker-php-ext-enable redis

# install XDebug but without enabling
RUN pecl install xdebug

# Install the PHP zip extention
RUN docker-php-ext-install zip

FROM base as target

#####################################
# Install necessary libraries
#####################################
RUN apk add --no-cache \
freetype \
jpeg \
icu \
libzip

#####################################
# Copy extensions from build stage
#####################################
COPY --from=build /usr/local/lib/php/extensions/no-debug-non-zts-20210902/* /usr/local/lib/php/extensions/no-debug-non-zts-20210902
COPY --from=build /usr/local/etc/php/conf.d/* /usr/local/etc/php/conf.d

#####################################
# Composer
#####################################
RUN curl -s http://getcomposer.org/installer | php && \
mv composer.phar /usr/local/bin/composer
# Project image
FROM php-base-image

#####################################
# Entrypoint
#####################################
COPY ./docker/php-fpm/docker-entrypoint.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
RUN ln -s /usr/local/bin/docker-entrypoint.sh /

WORKDIR /var/www/html
COPY . /var/www/html/

RUN composer install

ENTRYPOINT ["docker-entrypoint.sh"]
CMD ["docker-entrypoint.sh"]

Now the base image is only about 160M and our project image really only contains what is needed to run the project, not bloat. Furthermore, the base image maybe uploaded to docker hub and used in many more projects without having to recompile all the PHP extensions everytime. Of course the base image can be build upon PHP 8.1, 8.2 or any later version of PHP by choosing a different image tag as a base.

In short, this approach has proven to cut a lot of time building Docker images and to help improving maintainability.

As a side note, there is an easier way to build a base PHP image with alpine, by installing PHP using apk add php81 and the extensions using apk add php-extension However, this approach didn’t work for one specific extension php-iconv, which was needed at the time this article was written. This is a known problem with alpine that is waiting for a resolution.

In conclusion, although this example might be over the top for most applications, it clearly demonstrate the power of Docker multi-stage builds and its advantages. Also, it shows that even apcu can still be useful!

--

--