Using ZendPHP via Docker

This topic describes the procedures for using ZendPHP from a Docker image. ARM images are available where supported.

The Docker ecosystem provides a number of tools for container orchestration. Zend by Perforce provides templates for Docker Compose, Docker Swarm/Stack, and Kubernetes (k8s) demonstrating how to set up a session cluster using ZendPHP, a web server, and Redis. You can download these templates from https://www.zend.com/downloads/zendphp-docker-templates.

If you are familiar with using Docker to install or update applications, use the information in the table below to get started; otherwise, continue reading for more detailed information on using ZendPHP from a Docker image.

Zend Container Registry https://cr.zend.com
Login Login first using docker login -u {order-id} cr.zend.com before using the pull command
pull command docker pull cr.zend.com/zendphp/{PHP_VERSION}:{OS}-{OS_VERSION}-{specialties}

ARM images have the suffix "-arm64" in their tag names.

Use a pre-built ZendPHP image

Zend provides some pre-built images for Ubuntu, CentOS, and Debian in our container registry.

  1. Login to Zend's container registry using the credentials provided to you.

    docker login cr.zend.com

  2. Pull the image. For example, to pull the FPM specialty for PHP 7.4 on Ubuntu 20.04 using the ARM architecture, use the following command:

    docker pull cr.zend.com/zendphp/7.4:ubuntu-20.04-fpm-arm64

    Note Use the docker-ls tool to get a full list of the images Zend provides. If you do not see the one you need, please let us know.

After pulling an image you are now ready to run it. See the Using Docker images section for more information on using images.

Build-argument customizable image

If you would prefer to build your own image, Zend provides a customizable Dockerfile.

  1. Download the customizable Dockerfile or you can copy and paste the file from Custom Dockerfile.

  2. Build the image using the docker build command. The list of build arguments is included at the end. For example:

    docker build -f Dockerfile.Custom -t zendphp-8.0-fpm --build-arg OS=debian \ --build-arg OS_VERSION=10 --build-arg ZENDPHP_VERSION=8.0 --build-arg BASE_IMAGE=fpm \ --build-arg "ZEND_EXTENSIONS_LIST=mysqli pdo pdo-mysql tidy" \ --build-arg POST_BUILD_BASH=prepare.sh .

    Note For this example to work, you MUST have at least one .sh script present for the custom image (due to the COPY *.sh /usr/local/sbin line); either that, or you should omit it if there are no additional install.sh tasks.
  3. After building the image, you can use docker run to create the container. For example, to create a container from the image you just built you might use:

    docker run -dp 3000:3000 zendphp

After building an image you are now ready to run it. See the Using ZendPHP Docker images section for more information on using images.

Using built images

The following sections are relevant to all built images and cover changes that occurred when introducing the ability to run rootless images.

zendphp user and group

Images create the following user and group:

  • User zendphp, with UID 10000

  • Group zendphp, with GID 10001

php-fpm images always run worker processes as the zendphp user.

ZendPHPCustomizeWithBuildArgs.sh

All images include the script ZendPHPCustomizeWithBuildArgs.sh, which can be invoked in Dockerfile extensions that build FROM ZendPHP base images.

The script uses build arguments to customize the image, including:

  • Setting up the container's system timezone, including in PHP configuration.

  • Setting up ZendPHP package repository credentials (required in order to install additional extensions).

  • Installing additional system packages.

  • Installing pre-packaged PHP extensions from the ZendPHP package repository.

  • Installing PECL extensions.

  • Installing Composer.

  • Installing php-fpm configuration, using either a development or production profile.

  • Running a custom post-build script.

  • Setting up the container to run as the zendphp user (instead of root).

Build arguments

The ZendPHPCustomizeWithBuildArgs.sh script uses the following Docker build arguments:

  • TIMEZONE: Timezone to use within the image. This is set to UTC by default.
  • ZENDPHP_REPO_USERNAME and ZENDPHP_REPO_PASSWORD: The username/order ID and password associated with your ZendPHP package repository credentials. When both are specified, the values are used to setup access credentials to the ZendPHP package repository within your image, allowing you to install additional ZendPHP packages.
  • SYSTEM_PACKAGES: Space or comma-separated list of additional system packages to install. This is not set by default.
  • ZEND_EXTENSIONS_LIST: Space or comma-separated list of additional ZendPHP packages to install (e.g., mysqli, pgsql, and so forth). This is not set by default. Do not include any package prefix. (Packages are prefixed based on the OS, and generally follow the format php7.4-zend or php74zend-.)
  • PECL_EXTENSIONS_LIST: Space or comma-separate list of PECL extensions to install using the PECL tool. This is not set by default.
  • INSTALL_COMPOSER: Select whether or not to add Composer to the image. It is not set by default. Set it to any non-empty value if you wish to add Composer to the image.
  • ZEND_PROFILE: (php-fpm images only) Set to dev or development to use a development php-fpm configuration instead of production settings. Any values other than dev or development, including no value, enables a production configuration.
  • POST_BUILD_BASH: Fully qualified path to a shell script, or name of a shell script located under /usr/local/sbin/ in the container. When present, ZendPHPCustomizeWithBuildArgs.sh runs the POST_BUILD_BASH script as a final setup step.
  • RUN_FPM_AS_ZENDPHP_USER and RUN_FPM_AS_ZENDPHP_USER_APPDIR: These settings cause the php-fpm binary manager process to execute under the zendphp user instead of under root. To do this, RUN_FPM_AS_ZENDPHP_USER must be a non-empty value and RUN_FPM_AS_ZENDPHP_USER_APPDIR should point to the application root. When enabled, ZendPHPCustomizeWithBuildArgs.sh changes ownership of all files under the specified application directory to the zendphp user.

Custom Dockerfile

Zend provides you with a customizable Dockerfile you can use to create a custom image. You can download the file from this page or copy it from here:

# OS=one of ubuntu, centos, or debian ARG OS=ubuntu # OS_VERSION=OS version to use; e.g., 20.04, 8, and 10 ARG OS_VERSION=20.04 # PHP version to use; can be 5.6, 7.1, 7.2, 7.3, 7.4, or 8.0. # Ability to build a version is dependent on having ZendPHP credentials that # authorize use of that version. ARG ZENDPHP_VERSION=7.4 # BASE_IMAGE=cli or fpm ARG BASE_IMAGE=fpm # Append -arm after $BASE_IMAGE if you wish to use the ARM variant FROM cr.zend.com/zendphp/${ZENDPHP_VERSION}:${OS}-${OS_VERSION}-${BASE_IMAGE} # CUSTOMIZATIONS # # TIMEZONE=timezone the OS should use; UTC by default ARG TIMEZONE=UTC # INSTALL_COMPOSER=true or false/empty (whether or not Composer will be installed in # the image) ARG INSTALL_COMPOSER= # SYSTEM_PACKAGES=space- or comma-separated list of additional OS-specific # packages to install (e.g., cron, dev libraries for building extensions, etc.) ARG SYSTEM_PACKAGES # ZEND_EXTENSIONS_LIST=space- or comma-separated list of packaged ZendPHP # extensions to install and enable. These should omit the prefix # php{VERSION}-zend (DEB) or php{VERSION}zend (RPM); e.g., mysqli, pdo-pgsql, etc. ARG ZEND_EXTENSIONS_LIST # PECL_EXTENSIONS_LIST=space- or comma-separated list of PECL extensions to # compile, install, and enable. You will need to install the dev/devel package # for the ZendPHP version you are using, and any additional devel libraries # that may be required. ARG PECL_EXTENSIONS_LIST # POST_BUILD_BASH=full path to a bash script or name of a bash script under # /usr/local/sbin to execute following build tasks. You will need to ADD or COPY # these to the image before calling ZendPHPCustomizeWithBuildArgs.sh, and ensure # they are executable. Such scripts can be used to further customize your image. ARG POST_BUILD_BASH # Prepare tzdata 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. # Customize PHP runtime according to the given building arguments. # Generally, this should be the last statement of your custom image. RUN ZendPHPCustomizeWithBuildArgs.sh

s6 overlay

All ZendPHP images contain the s6 overlay, which is a container-specific version of the s6 process supervision suite that provides additional features such as hooks for changing file system permissions on initialization, firing arbitrary initialization scripts, and firing finalization scripts on container shutdown.

CLI images do not use s6 by default, but FPM images do. However, CLI images do install the s6 overlay, so you can make use of it in your custom CLI images, which can be useful when writing daemons in PHP, particularly when using an async runtime such as OpenSwoole, ReactPHP, or AMPHP.

Using the s6 overlay provides the following benefits:

  • Process segregation. This provides an extra layer of security that helps prevent malicious actors from breaking out of the container.

  • Process supervision. s6 restarts daemons when they die, automatically.

  • Ability to run daemons as non-privileged users.

  • Ability to run multiple daemons in a single container, and supervise each.

We recommend reading the s6 overlay documentation to understand the full range of features that are available to you.

Running additional daemons

Writing a daemon involves the following:

  • Creating a directory for the daemon under /etc/services.d.

  • Adding an executable file named run under your new directory.

For example, if you want to run cron on your system you would create the directory /etc/services.d/crond/, place a file named run inside it, and make the file executable (chmod 755 /etc/services.d/crond/run).

The contents of that file might look like:

#!/bin/sh set -e echo "Starting cron daemon" if [ -x /usr/sbin/crond ];then # RHEL systems crond -n elif [ -x /usr/sbin/cron ];then # DEB systems cron -f else echo "FAILED TO FIND CRON DAEMON!" exit 1 fi

Note that these start the cron daemon in foreground mode and not background mode; this is done so that s6 can monitor them and restart them if necessary.

You could also use the s6 execlineb shell to write this script:

#!/usr/bin/execlineb -P # Debian system cron cron -f

Running daemons as non-privileged users

In order for a daemon to run as a non-privileged user, you can make use of the s6-setuidgid utility in your /etc/services.d run scripts. We recommend using execlineb for such applications.

As an example, to run the previous crond example as the zendphp user:

#!/usr/bin/execlineb -P s6-setuidgid zendphp cron -f

Note When you provide the RUN_FPM_AS_ZENDPHP_USER and RUN_FPM_AS_ZENDPHP_USER_APPDIR build arguments, we install a services.d run script that does the above for the php-fpm process.

Entrypoint.d and cont-init.d

Previous versions of ZendPHP images provided an /entrypoint.d/ directory for both executable and non-executable shell scripts. On container start, executable shell scripts in this directory are executed and non-executable shell scripts are sourced.

Since the addition of the s6 overlay to ZendPHP images, we recommend using the /etc/cont-init.d directory for executable shell scripts, while keeping any sourced shell scripts (e.g., those used to set environment variables and/or define globally available shell functions) in /entrypoint.d/.

Using ZendPHP Docker images

After building a ZendPHP docker image, you need to run it. In the case of a CLI image, this is a no-op; you need to specify a command in the container to run. For FPM images, running the image starts a php-fpm pool.

CLI images

CLI images specify the PHP binary as the container entrypoint. As such, you can run the image with arguments to the binary.

  1. Report the PHP version:

    docker run cr.zend.com/zendphp/7.4:debian-10-cli -v

  2. Run the PHP REPL:

    docker run cr.zend.com/zendphp/7.4:debian-10-cli -a

  3. Run a PHP script. The following example runs the script test.php from the current working directory:

    docker run -it -v $(realpath ${PWD}):/var/www -w /var/www cr.zend.com/zendphp/7.4:debian-10-cli test.php

  4. Run Composer:

    docker run -it -v $(realpath ${PWD}):/var/www -w /var/www cr.zend.com/zendphp/7.4:debian-10-cli composer install

Consuming an FPM image

By default, Zend's FPM images configure a PHP-FPM pool that runs on port 9000, and the image exposes port 9000. This is true even of images you create using Zend's custom Dockerfile. For this, you need an Apache, nginx, or other webserver sitting in front of your ZendPHP FPM image(s) in order to consume them. These can be their own Docker images or be standalone servers running elsewhere and connecting to your instances.

The ZendPHP FPM containers contain a healthcheck script, /usr/local/bin/fpm-healthcheck.sh. Your container orchestration can use this script to verify that the container is healthy and capable of receiving requests.

The following example demonstrates a docker-compose.yml healthcheck configuration for a PHP-FPM service:

healthcheck: test: /usr/local/bin/fpm-healthcheck.sh interval: 30s timeout: 10s retries: 5 start_period: 30s

Via Apache

Your Apache instance needs the mpm_event module enabled (and the mpm_prefork module disabled). Additionally, it requires the proxy and proxy_fcgi modules. Refer to the Apache manual and/or your distribution's tooling for information on how to accomplish these tasks.

From there, create a configuration file in the conf.d subdirectory of your Apache configuration; generally, this is /etc/httpd/conf.d, /etc/apache2/conf-enabled, or similar. Name the file php-fpm.conf and create it with the following contents:

# Allow php to handle Multiviews # AddType text/html .php # Add index.php to the list of files that will be served as directory # indexes. DirectoryIndex index.php # Enable http authorization headers SetEnvIfNoCase ^Authorization$ "(.+)" HTTP_AUTHORIZATION=$1 # Redirect to php-fpm <FilesMatch \.(php|phar)$> SetHandler "proxy:fcgi://{HOSTNAME}:9000" </FilesMatch>

Substitute {HOSTNAME} with the name of the server or container exposing php-fpm.

Once your changes are made, restart your web server, or fire up your container with the Apache instance, and verify that you can reach PHP pages in your application.

Via nginx

In an nginx virtual host definition, you can tell the server to proxy to the php-fpm instance by matching any .php file. As an example:

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

Substitute {HOSTNAME} with the name of the server or container exposing php-fpm.

Once your changes are made, restart your web server, or start your container with the nginx instance, and verify that you can reach PHP pages in your application.

Using the custom Dockerfile with docker-compose

One benefit to the custom Dockerfile is that you can use the same file with multiple services, which is particularly useful when using orchestration via docker-compose.

As an example, the following sets up a ZendPHP PHP-FPM container with a development profile for PHP 7.4 using Debian 10; the container installs the mbstring, zip, and mysql extensions and includes Composer. It also includes an nginx image that depends on the PHP image and proxies to it using a custom vhost definition specified in the application itself.

version: '3' services: php: build: context: . dockerfile: Dockerfile args: OS: 'debian' OS_VERSION: '10' ZENDPHP_VERSION: '7.4' BASE_IMAGE: 'fpm' TIMEZONE: 'UTC' ZEND_EXTENSIONS_LIST: 'mbstring,zip,mysql' ZEND_PROFILE: 'dev' INSTALL_COMPOSER: 1 env_file: - .env volumes: - .:/var/www nginx: image: nginx:latest depends_on: - php ports: - "8080:80" env_file: - .env volumes: - .:/var/www - ./data/nginx/dev.conf:/etc/nginx/conf.d/default.conf

If you needed another PHP service, you could add it to your docker-compose.yml and have it reference the same Dockerfile, but with different build arguments:

version: '3' services: php: build: context: . dockerfile: Dockerfile args: OS: 'debian' OS_VERSION: '10' ZENDPHP_VERSION: '7.4' BASE_IMAGE: 'fpm' TIMEZONE: 'UTC' ZEND_EXTENSIONS_LIST: 'mbstring,zip,mysql' ZEND_PROFILE: 'dev' INSTALL_COMPOSER: 1 env_file: - .env volumes: - .:/var/www api: build: context: . dockerfile: Dockerfile args: OS: 'debian' OS_VERSION: '10' ZENDPHP_VERSION: '8.0' BASE_IMAGE: 'fpm' TIMEZONE: 'UTC' ZEND_EXTENSIONS_LIST: 'mbstring,zip,intl' ZEND_PROFILE: 'dev' env_file: - .env volumes: - ./api:/api nginx: image: nginx:latest depends_on: - php ports: - "8080:80" env_file: - .env volumes: - .:/var/www - ./data/nginx/dev.conf:/etc/nginx/conf.d/default.conf

Note Note that the api service uses a different PHP version and a different list of extensions.

Environment Variables

You can provide environment variables when starting the container, or in your compose or kubernetes configuration.

The following optional environment variables are supported:

  • ZENDHQD_URI
    Define this variable to indicate the URI to the ZendHQ service.
    Examples:
    • tcp://zendhq:10900
    • tcp://10.0.1.250:10900

 

ZendPHP User and Group

The Docker images create the following users and groups:

  • A “zendphp” user with UID 10000

  • A “zendphp” group with GID 10001

Use this user and group to set permissions of directories and files mounted through volumes, for example, a cache directory in the application.