ZendPHP Docker Tips and Tricks
March 1, 2022

PHP Docker Images Tips and Tricks

PHP Development

In a previous post, I detailed Docker orchestration with PHP, and noted that Zend now offers its own container registry with ZendPHP images. We also offer a downloadable, flexible, customizable Dockerfile for ZendPHP images (found here) that can greatly simplify image creation, particularly when using orchestration technologies.

In this post, I'll detail that Dockerfile, showing how you can use the same file to create different custom PHP Docker images, as well as demonstrate some additional techniques you can use.

What Is a Dockerfile?

A Dockerfile is a text file that describes how to build the initial state of a container or image. Most consumers will extend an existing Docker image and add additional instructions to it to provide settings specific to their own application. Base images can be an operating system (e.g., Ubuntu 20.04, Alpine Linux, or CentOS 8), or another image built on these.

Can I run PHP in Docker?

Often, a base image will provide a language runtime on top of a base operating system image, ensuring that you can run applications that target that language within that operating system. PHP is no different, and there are a number of different PHP images you can target in your application Dockerfile. We supply our own base images for ZendPHP, and provide extensive tooling to make configuring your PHP runtime easier, reducing the amount of knowledge you need to be successful with PHP in Docker.

The Dockerfile

An uncommented version of the Dockerfile looks like the following:

ARG OS=ubuntu
ARG OS_VERSION=20.04
ARG ZENDPHP_VERSION=7.4
ARG BASE_IMAGE=fpm

FROM cr.zend.com/zendphp/${ZENDPHP_VERSION}:${OS}-${OS_VERSION}-${BASE_IMAGE}

ARG TIMEZONE=UTC
ARG INSTALL_COMPOSER=
ARG SYSTEM_PACKAGES
ARG ZEND_EXTENSIONS_LIST
ARG PECL_EXTENSIONS_LIST
ARG POST_BUILD_BASH
ENV TZ=$TIMEZONE \
    YUM_y='-y'
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone

# ADD or COPY any files or directories needed in your image here.

RUN ZendPHPCustomizeWithBuildArgs.sh

The above Dockerfile will build a PHP-FPM 7.4 image using Ubuntu 20.04, using the base extension list.

You'll notice that you can customize it using a variety of build arguments:

OSThe operating system. We have Ubuntu, CentOS, and Debian builds available currently in our container registry.
OS_VERSIONThe version of the operating system. We support Ubuntu 20.04, Debian 10, and CentOS 7 and 8 currently in our container registry.
ZENDPHP_VERSIONWhich ZendPHP version you want to install: 5.6, 7.1, 7.2, 7.3, 7.4, 8.0, or 8.1 are all available, though LTS version (all prior to 7.4) require some additional setup in order to work, as they require Zend licenses.
BASE_IMAGEOne of "cli" or "fpm". CLI versions provide a build with only the PHP CLI binary, while FPM versions provide both the CLI and PHP-FPM binaries. The CLI versions are often useful in CI/CD pipelines for things such as running PHPUnit, PHPCS, or static analysis. PHP-FPM binaries can be used behind any web server that supports FastCGI.
TIMEZONEThe system timezone to use. We recommend using UTC, but if you need to use a local timezone, it can be set via a build argument.
INSTALL_COMPOSERIf a "truthy" value, the image will install Composer during build.
SYSTEM_PACKAGESA comma- or space-separated list of additional system packages to install, if any.
ZEND_EXTENSIONS_LISTA comma- or space-separated list of additional PHP extensions available in the ZendPHP repository to install; these can be specified using just the extension name (e.g. "mysqli") instead of the full package name.
PECL_EXTENSIONS_LISTA comma- or space-separated list of PECL extensions to build and install. These can be just the PECL extension name (e.g., "inotify"), the name and version (e.g., "inotify-0.1.6"), and/or a prefix indicating the initialization priority once installed (e.g., "30-swoole"; default priority is 20, with lower priorities initializing earlier).
POST_BUILD_BASHA shell script or other executable to execute as the last build step. This can be useful for doing things such as moving files around, setting permissions, or other tasks required to ensure the container is ready to run. 

These are a lot of build arguments, and you likely won't want to specify them on the command line regularly:

$ docker build \
> --build-arg OS=debian \
> --build-arg OS_VERSION=10 \
> --build-arg ZENDPHP_VERSION=8.1 \
> --build-arg ZEND_EXTENSIONS_LIST=mysqli,gd,intl \
> --build-arg POST_BUILD_BASH=setup.sh \
> -f Dockerfile.custom \
> -t mybiz/php-fpm-8.1

Because you will have no application files present, just an empty PHP install, let's figure out ways to customize the image.

Customizing PHP Docker Images

When performing orchestration with Docker, you will generally use another technology such as Compose, Swarm, or Kubernetes. These technologies each allow you to describe services and (generally speaking) these descriptions allow you to specify either an image to consume and customize with volumes, networks, and environment variables, or a Dockerfile and a build context (which can include things such as build arguments). It's this latter that I want to look at now.

To keep the examples simple, I'm going to use Compose here. However, they can be extrapolated to Swarm, Kubernetes, Helm, and other solutions.

We'll use the base Dockerfile, and provide configuration for the build.

services:
  php:
    build: 
      context: .
      dockerfile: Dockerfile
      args:
        OS: debian
        OS_VERSION: 10
        ZENDPHP_VERSION: 8.1
        INSTALL_COMPOSER: 'true'
        ZEND_EXTENSIONS_LIST: 'bz2 curl intl mbstring opcache xsl zip'
        POST_BUILD_BASH: '/var/www/scripts/setup.sh'
    volumes:
      - .:/var/www/

The above will:

  • Create a PHP-FPM 8.1 image using Debian 10
  • Install Composer
  • Add and enable the bz2, curl, intl, mbstring, opcache, xsl, and zip extensions
  • Map the current directory to /var/www
  • Run a post-build script relative to the mapped directory

This sort of approach allows you to become immediately productive in development. You can tweak the extensions list, change PHP versions, and more with minor edits.

Here's a Compose file that describes a container utilizing OpenSwoole instead:

version: '3.3'

services:
  php:
    build: 
      context: .
      dockerfile: Dockerfile
      args:
        OS: ubuntu
        OS_VERSION: 20.04
        ZENDPHP_VERSION: 8.1
        BASE_IMAGE: cli
        INSTALL_COMPOSER: 'true'
        ZEND_EXTENSIONS_LIST: 'bz2 curl intl mbstring opcache xsl zip'
        PECL_EXTENSIONS_LIST: '30-openswoole-4.9.0'
        POST_BUILD_BASH: '/var/www/scripts/setup.sh'
    volumes:
      - .:/var/www/

Be aware that when using PECL_EXTENSIONS_LIST, you may need to also specify development packages in the SYSTEM_PACKAGES build argument required to build the extension. When performing PECL installs, the ZendPHP development package (e.g., php-7.4-zend-dev or php74-zend-php-devel) and/or PEAR package will also be installed during build time.

The beauty of each of these is that I can use the same Dockerfile over and over, without changes. By using build arguments and environment variables, I am able to customize the image I build.

Building PHP Docker Images for Production

The previous examples were a good start. However, in production, we generally want to ensure that application files are copied into the image, and not mounted via a volume. This is particularly true when we consider multi-container and/or multi-server orchestrations.

As such, we'll need to customize the Dockerfile to do so.

If you pay close attention to the basic Dockerfile, you'll note that it has very few directives; the bulk of it is defining build arguments. In most cases, you can provide your customizations in one of two locations:

  • Immediately before the call to ZendPHPCustomizeWithBuildArgs.sh.
  • Immediately following the call to ZendPHPCustomizeWithBuildArgs.sh.

As an example, I can copy the application files in:

ARG OS=ubuntu
ARG OS_VERSION=20.04
ARG ZENDPHP_VERSION=7.4
ARG BASE_IMAGE=fpm

FROM cr.zend.com/zendphp/${ZENDPHP_VERSION}:${OS}-${OS_VERSION}-${BASE_IMAGE}

ARG TIMEZONE=UTC
ARG INSTALL_COMPOSER=
ARG SYSTEM_PACKAGES
ARG ZEND_EXTENSIONS_LIST
ARG PECL_EXTENSIONS_LIST
ARG POST_BUILD_BASH
ENV TZ=$TIMEZONE \
    YUM_y='-y'
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone

# ADD or COPY any files or directories needed in your image here.
COPY . /var/www/

RUN ZendPHPCustomizeWithBuildArgs.sh

A huge benefit to this is that we can likely still re-use the Dockerfile with multiple applications. We can assume that at both build and runtime, the application is installed to /var/www/, allowing us to specify post-build scripts, or configure our FastCGI-enabled webserver to pass requests to that location. In development, we can use Compose, where we can map a volume so that as we make changes, they're reflected in the running application.

Multi-Stage Builds

In a previous example, I noted that when specifying PECL_EXTENSIONS_LIST, I may need to specify system development packages in order to build the extension. In production, I likely don't want those lingering.

One way to deal with those is to add another build step that removes the development packages:

RUN apt-get remove {list of development packages here}

Another way is to ensure they're never present in the first place, which can be done with a multi-stage build. As an example, I recently wanted to build OpenSwoole for a ZendPHP container. I have one stage that performs the build, which requires additional packages. The production image then copies files from that stage into itself.

# DOCKER-VERSION        1.3

# Build Swoole
FROM cr.zend.com/zendphp/8.1:ubuntu-20.04-cli as swoole

## Prepare image
ARG SWOOLE_VERSION=4.9.0
ARG TIMEZONE=UTC
ENV TZ=$TIMEZONE
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone

RUN set -e; \
    apt-get update; \
    apt-get install -y php8.1-zend-dev libcurl4-openssl-dev; \
    mkdir /workdir; \
    cd /workdir; \
    curl -L -o swoole-src-${SWOOLE_VERSION}.tgz https://github.com/openswoole/swoole-src/archive/refs/tags/v${SWOOLE_VERSION}.tar.gz; \
    tar xzf swoole-src-${SWOOLE_VERSION}.tgz; \
    cd swoole-src-${SWOOLE_VERSION}; \
    phpize; \
    ./configure \
        --enable-swoole \
        --enable-sockets \
        --enable-http2 \
        --enable-openssl \
        --enable-swoole-json \
        --enable-swoole-curl; \
    make; \
    make install

# Build the PHP container
FROM cr.zend.com/zendphp/8.1:ubuntu-20.04-cli

## Install Swoole
COPY --from=swoole /usr/lib/php/8.1-zend/openswoole.so /usr/lib/php/8.1-zend/openswoole.so
COPY --from=swoole /usr/include/php/8.1-zend/ext/openswoole /usr/include/php/8.1-zend/ext/openswoole
RUN set -e; \
    echo "extension=openswoole.so" > /etc/zendphp/cli/conf.d/60-swoole.ini

## Customizations
ARG TIMEZONE=UTC
ARG INSTALL_COMPOSER=false
ARG SYSTEM_PACKAGES
ARG ZEND_EXTENSIONS_LIST
ARG PECL_EXTENSIONS_LIST
ARG POST_BUILD_BASH

## Prepare tzdata
ENV TZ=$TIMEZONE
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone

## Customize PHP runtime according
## to the given building arguments
RUN ZendPHPCustomizeWithBuildArgs.sh

COPY . /var/www/

## Set working directory
WORKDIR /var/www

This approach ensures that I have no build tools in my final image, only the results of building (the extension file and its libraries). The Dockerfile can still be re-used for any OpenSwoole-based images I want to expose, and I now get the benefit of a Docker build caching whenever I create such images, as they will cache the "swoole" stage.

The Benefits of ZendPHP Docker Images

The primary benefit of ZendPHP Docker images is the ease of customization: you can use the same Dockerfile and create custom images via build arguments, which can be specified in orchestration files and kept in your version control repositories. This approach allows you to create standard images re-usable in many contexts, with minimal effort.

And this article just scratches the surface of what you can do with ZendPHP Docker images! In addition to the features shown here, we also have tools to allow running multiple services within your containers safely, as well as mechanisms for creating running rootless, or non-privileged, containers. Stay tuned for more on that in upcoming blogs.

Get Started Now

So, what are you waiting for? Get started with ZendPHP Docker images today!

TRY ZENDPHP FREE