A series of Docker images to run PHP Applications on Usabilla Style
- Using and extending
- Architecture Decisions Records
- Basic architecture
- The base images
- Alpine Linux situation
- The available tags
- Adding more supported versions
- Prometheus Exporter
- Dockerfile example with Buildkit
- PHP FPM functional example
- Contributing
- License
This project adheres to ADRs, a list can be found here.
Our policy on versions we support is outlined in ADR0005.
All being based on the official images we provide:
- PHP cli - Compiled without php-fpm, a simple php binary image
- PHP fpm - Specifically designed to share the php-fpm socket towards a fastcgi compliant web sever
- Nginx - Meant for PHP projects built on the PHP FPM image in this repository, since it looks for a php-fpm socket and doesn't have access to the PHP code
The fpm/HTTP server relationship:
+------------------------------+-----+ +--------------------+
| | :80 | | |
| FastCGI HTTP container +-----+ | PHP fpm container |
| | | |
| | | |
| +-------------------------+ | | +--------------+ |
| | /var/run/php|fpm.sock |<--------------+ | Source code | |
| +-------------------------+ | | +--------------+ |
| | | |
+------------------------------------+ +--------------------+
- PHP
- Shush - Used for decrypting our secret variables using AWS KMS
- composer - To provide the installation of PHP projects
- Nginx
- A location.d helper to enable/disable custom location configuration
All images are based on their official variants, being:
- PHP official image
- Nginx official image
- Both PHP and Nginx images are based on Alpine Linux
Even though both of the images are based on the Alpine Linux, the PHP official repository gives us the option to choose
between its versions, at this moment being 3.9
or 3.10
.
Meanwhile on the official Nginx images we have no control over which Alpine version we use, this explains the tagging strategy coming in the next section.
The docker registry prefix is usabillabv/php
, thus usabillabv/php:OUR-TAGS
.
In order to provide upgrade path we intend to keep one or more versions of PHP and Nginx.
Currently Available tags on Docker hub
The tag naming strategy consists of (Read as a regex):
- PHP:
(phpMajor).(phpMinor)-(cli|fpm)-(alpine|future supported OSes)(alpineMajor).(alpineMinor)(-dev)?
- Example:
7.3-fpm-alpine3.11
,7.3-fpm-alpine3.11-dev
- Note: The minor version might come followed by special versioning constraints in case of betas, etc. For instance
7.3-rc-fpm-alpine3.11-dev
- Example:
- Nginx:
nginx(major).(minor)(-dev)?
- Example:
nginx1.15
,nginx1.15-dev
- Example:
The whole CI/CD pipeline is centralized in Makefile targets, the build of cli, fpm and http (for now only nginx) images
have their targets named as build-cli
, build-fpm
and build-http
.
With the help of building scripts the addition of new versions is as easy as updating the Makefile with the desired new version.
All the newly built versions are going to be automatically tagged and pushed upon CI/CD success, to see the output of
your new changes you can see the (BUILD).tags
file in the tmp
directory.
In this example adding PHP 7.4-rc for cli and fpm:
build-cli: clean-tags
./build-php.sh cli 7.4 3.10
./build-php.sh cli 7.4 3.11
+ ./build-php.sh cli 7.4-rc 3.12
build-fpm: clean-tags
./build-php.sh fpm 7.4 3.10
./build-php.sh fpm 7.4 3.11
+ ./build-php.sh fpm 7.4-rc 3.12
Being ./build-php.sh (cli/fpm) (PHP version) (Alpine version)
In this example adding Nginx 1.16:
build-http: clean-tags
./build-nginx.sh 1.15 nginx
./build-nginx.sh 1.14
+ ./build-nginx.sh 1.16
Being ./build-nginx.sh (nginx version) (extra tag)
Note you can add extra tags, this means if you want to make Nginx 1.16 our new default version you have to:
build-http: clean-tags
- ./build-nginx.sh 1.15 nginx
+ ./build-nginx.sh 1.15
./build-nginx.sh 1.14
- ./build-nginx.sh 1.16
+ ./build-nginx.sh 1.16 nginx
Removing a version from the build will not remove it from the Docker registry, this has to be done manually when desired.
This image ships with the php-fpm-healthcheck which allows you to healthcheck FPM independently of the Nginx setup, providing more compatibility with the single process Docker container.
This healthcheck provides diverse metrics to watch and can be configured according to your needs. More information on how to use it can be found in the official documentation.
The healthcheck can be found in the container $PATH
as an executable:
$ php-fpm-healthcheck
$ echo $?
0
Simply use the images as base of the application's Dockerfile
and apply the necessary changes.
# syntax=docker/dockerfile:1.0.0-experimental
FROM usabillabv/php:7.4-fpm-alpine3.12
In usual cases it might not be necessary to extend the nginx images, unless you desire to extend its behavior, for instance to serve static files.
Nginx is configured with only one virtual host, which is using /opt/project/public
as the document root. Ideally we
should change this configuration to point to the public
directory of our project, so that we expose only what's
necessary.
In order to do this you should override NGINX_DOCUMENT_ROOT
environment variable in the Dockerfile
, e.g.:
# assuming that your project is mounted/copied to `/project` and it has a public
# directory...
ENV NGINX_DOCUMENT_ROOT="/project/public"
The default server name is localhost
and that can also be overridden using an environment variable
(NGINX_SERVER_NAME
) in the Dockerfile
, e.g.:
ENV NGINX_SERVER_NAME="myawesomeservice myawesomeservice.usabilla.com"
To use the most of your server you can tweak the number of nginx workers and connections accepted by them, the default
values are (respectively): 1
and 1024
.
These values can be overridden using environment variables (NGINX_WORKERS_PROCESSES
and NGINX_WORKERS_CONNECTIONS
)
in the Dockerfile
, e.g.:
ENV NGINX_WORKERS_PROCESSES="4"
ENV NGINX_WORKERS_CONNECTIONS="2048"
Documentation for worker_processes and worker_connections.
The default keepalive_timeout
is 75
and that can also be overridden using an environment variable
(NGINX_KEEPALIVE_TIMEOUT
) in the Dockerfile
, e.g.:
ENV NGINX_KEEPALIVE_TIMEOUT="30"
More about it in the Official documentation.
The default client_body_buffer_size
is 8k|16k
(depending on architecture), having it configurable helps to not
create disk body buffers in apps that don't splitting it, e.g.:
ENV NGINX_CLIENT_BODY_BUFFER_SIZE="64k"
More about it in the Official documentation.
The default client_max_body_size
is 1m
, you can increase it in case of
larger payloads, e.g.:
ENV NGINX_CLIENT_MAX_BODY_SIZE="8m"
More about it in the Official documentation.
The default large_client_header_buffers
is 4 8k
, being number size
you can
increase it in case of larger header payloads, e.g.:
ENV NGINX_LARGE_CLIENT_HEADER_BUFFERS="8 128k"
More about it in the Official documentation.
By default we are not exposing the nginx version in the Server
header, that
can also be overridden using an environment variable (NGINX_EXPOSE_VERSION
)
in the Dockerfile
, e.g.:
ENV NGINX_EXPOSE_VERSION="on"
There's a CORS helper available, it can be activated by running:
$ docker-nginx-location.d-enable cors
Or by setting an environment variable:
ENV NGINX_CORS_ENABLE=true
It's also possible to customize the Allow-Origin
but setting an environment variable in the Dockerfile
, e.g.:
ENV NGINX_CORS_ALLOW_ORIGIN="https://my-domain.cool"
To allow tuning the FPM pool, some pool directives are configurable via the following environment variables. For more information on these directives, see the documentation.
Directive | Environment Variable | Default |
---|---|---|
pm | PHP_FPM_PM | dynamic |
pm.max_children | PHP_FPM_PM_MAX_CHILDREN | 5 |
pm.start_servers | PHP_FPM_PM_START_SERVERS | 2 |
pm.min_spare_servers | PHP_FPM_PM_MIN_SPARE_SERVERS | 1 |
pm.max_spare_servers | PHP_FPM_PM_MAX_SPARE_SERVERS | 3 |
pm.process_idle_timeout | PHP_FPM_PM_PROCESS_IDLE_TIMEOUT | 10 |
pm.max_requests | PHP_FPM_PM_MAX_REQUESTS | 0 |
access.format | PHP_FPM_ACCESS_FORMAT | %R - %u %t "%m %r" %s |
An example Dockerfile with customized configuration might look like:
# syntax=docker/dockerfile:1.0.0-experimental
FROM usabillabv/php:7.3-fpm-alpine3.11
ENV PHP_FPM_PM="static"
ENV PHP_FPM_PM_MAX_CHILDREN="70"
ENV PHP_FPM_PM_START_SERVERS="10"
ENV PHP_FPM_PM_MIN_SPARE_SERVERS="20"
ENV PHP_FPM_PM_MAX_SPARE_SERVERS="40"
ENV PHP_FPM_PM_PROCESS_IDLE_TIMEOUT="35"
ENV PHP_FPM_PM_MAX_REQUESTS="500"
ENV PHP_FPM_ACCESS_FORMAT {\\\"cpu_usage\\\":%C,\\\"memory_usage\\\":%M,\\\"duration_microsecond\\\":%d,\\\"script\\\":\\\"%f\\\",\\\"content_length\\\":%l,\\\"request_method\\\":\\\"%m\\\",\\\"pool_name\\\":\\\"%n\\\",\\\"process_id\\\":\\\"%p\\\",\\\"request_query_string\\\":\\\"%q\\\",\\\"request_uri_query_string_glue\\\":\\\"%Q\\\",\\\"request_uri\\\":\\\"%r\\\",\\\"request_url\\\":\\\"%r%Q%q\\\",\\\"remote_ip_address\\\":\\\"%R\\\",\\\"response_status_code\\\":%s,\\\"time\\\":\\\"%t\\\",\\\"remote_user\\\":\\\"%u\\\"}
The official PHP images ship with recommended
ini
configuration files for both
development and production. In order to guarantee a reasonable configuration, our images load these files by default
in each image respectively at this path: $PHP_INI_DIR/php.ini
.
Images that wish to extend the ones provided in this repository can override these configurations easily by including
customized configuration files in the $PHP_INI_DIR/conf.d/
directory.
This image bundles helper scripts to manage PHP extensions (docker-php-ext-configure
, docker-php-ext-install
, and
docker-php-ext-enable
), so it's quite simple to install core and PECL extensions.
More about it in the Official Documentation.
To install a core extension that doesn't require any change in the way PHP is compiled you only need to use
docker-php-ext-install
, which will compile the extra extension and enable it.
To do it should include something like this to your Dockerfile
:
# Enables opcache:
RUN set -x \
&& apk add --no-cache gnupg \
&& docker-php-source-tarball download \
&& docker-php-ext-install opcache \
&& docker-php-source-tarball delete
# Installs PDO driver for PostgreSQL (temporarily adding postgresql-dev to have
# the necessary C libraries):
RUN set -x \
&& apk add --no-cache gnupg postgresql-client postgresql-dev \
&& docker-php-source-tarball download \
&& docker-php-ext-install pdo_pgsql \
&& docker-php-source-tarball delete \
&& apk del gnupg postgresql-dev
Some core extensions, like GD, requires changes to PHP compilation. For that you
should also use docker-php-ext-configure
, e.g.:
# Installs GD extension and the required libraries:
RUN set -x \
apk add --no-cache freetype-dev libjpeg-turbo-dev libpng-dev \
&& docker-php-ext-configure gd --with-freetype-dir=/usr/include/ --with-jpeg-dir=/usr/include/ \
&& docker-php-ext-install gd
Some extensions are not provided with the PHP source, but are instead available through PECL, see a full list of them here.
To install a PECL extension, use pecl install
to download and compile it, then use docker-php-ext-enable
to enable
it:
# Installs ast extension (temporarily adding the necessary libraries):
RUN set -x \
&& apk add --no-cache --virtual .phpize-deps $PHPIZE_DEPS \
&& pecl install ast \
&& docker-php-ext-enable ast \
&& apk del .phpize-deps
Check if the extension is loaded after building it:
$ docker build .
Successfully built 5bcf0f7d49b0
$ docker run --rm 5bcf0f7d49b0 php -m | grep ast
ast
# Installs MongoDB Driver (temporarily adding the necessary libraries):
RUN set -x \
&& apk add --no-cache --virtual .build-deps $PHPIZE_DEPS openssl-dev \
&& pecl install mongodb-1.5.3 \
&& docker-php-ext-enable mongodb \
&& apk del .build-deps
Some extensions are used across multiple projects but can have some complexities while installing so we ship helper scripts with the PHP images to install dependencies and enable the extension. The following helper scripts can be run inside projects' Dockerfile:
docker-php-ext-rdkafka
for RD Kafkadocker-php-ext-pdo-pgsql
for PDO Postgres
Since Xdebug is a common extension to be used we offer two options:
Use the dev
image by appending -dev
to the end of the tag, like: usabillabv/php:7.3-fpm-alpine3.11-dev
.
Not recommended if you're layering with your production images, using a different base image doesn't allow to you share cache among your Dockerfile targets.
We ship the image with a dev mode helper, which can install and configure Xdebug, as well as override the production
php.ini
with the recommended development version.
Installing and enabling the extensions
$ docker-php-dev-mode xdebug
As mentioned, we override the production php.ini
with the recommended development version, which can be found
here.
Next to that we provide some additional configuration to make it easier to start your debugging session. The contents of that configuration can be found here.
Both are enabled via the helper script, by running
$ docker-php-dev-mode config
Xdebug 3 comes with new mechanism to enable it's functionalities. The most notable, is the introduction of the
xdebug.mode
setting, which controls which features are enabled. It can be specified via .ini
files or by using the
environment variable XDEBUG_MODE
. To learn more about the different modes in which Xdebug can be configured, please
refer to the Xdebug settings guide.
With the introduction of the Xdebug mode in the v3 release, it is now mandatory to specify either xdebug.mode=coverage
setting in .ini
file, or XDEBUG_MODE=coverage
as environment variable, to use the code coverage analysis features. This impacts tools
like mutation tests.
We recommend setting the XDEBUG_MODE when booting up a new container. Here's an example on how it could look like:
docker run -it \
-e XDEBUG_MODE=coverage \
-v "<HOST_PATH>:<CONTAINER_PATH>" \
usabillabv/php:7.4-cli-alpine3.12-dev \
vendor/bin/infection --test-framework-options='--testsuite=unit' -s --threads=12 --min-msi=100 --min-covered-msi=100
Another notable change, is the Xdebug port change. The default port is now 9003
instead of 9000
. Check your IDE
settings to confirm the correct port is specified.
For the full upgrade guide, please refer to the official upgrade guide.
In order to monitor applications many systems implement Prometheus to expose metrics, one challenge specially in PHP is how to expose those to Prometheus without having to, either implement an endpoint in our application, or add HTTP and an endpoint for non-interactive containers.
This prove has the aim to provide support for the sidecar pattern for monitoring.
More about "Make your application easy to monitor" by Google
The easiest way to solve this problem in the PHP ecosystem is to make your application write down the metrics to a text file, which then is shared via a volume to a sidecar container which can expose it to Prometheus.
The container we offer is a simple Nginx based on the same configuration as the one for PHP-FPM, with the difference it only serves static content.
The image named prometheus-exporter-file
is available via our docker registry under with the tags (from less to more specific versions):
usabillabv/php:prometheus-exporter-file
- This has the behavior of latestusabillabv/php:prometheus-exporter-file1
usabillabv/php:prometheus-exporter-file1.0
# Pod v1 core Spec - https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.11/#pod-v1-core
spec:
template:
metadata:
annotations:
prometheus.io/path: /metrics
prometheus.io/port: "5556"
prometheus.io/scrape: "true"
spec:
containers:
- image: usabillabv/php:7.3-cli-alpine3.11
imagePullPolicy: IfNotPresent
volumeMounts:
- mountPath: /prometheus
name: prometheus-metrics
- image: usabillabv/php:prometheus-exporter-file1
imagePullPolicy: IfNotPresent
name: prometheus-exporter
env:
- name: NGINX_PORT
value: "5556"
ports:
- containerPort: 5556
name: http
protocol: TCP
volumeMounts:
- mountPath: /opt/project/public
name: prometheus-metrics
volumes:
- emptyDir: {}
name: prometheus-metrics
In this example the PHP container must write down the metrics in the file /prometheus/metrics
, the exporter container will have the same file mount at /opt/project/public/metrics
.
Which will then be available via http as http://pod:5556/metrics
, observe that the filename becomes the url which we configured the prometheus scrape to look for.
To be created and/or documented
For now please refer to: https://github.com/basvanbeek/opencensus-php-docker and https://github.com/census-instrumentation/opencensus-php
The Dockerfile in the example below is meant to centralize the production and development images in a single Dockerfile, sharing cached layers among the build steps, cleaning unnecessary files like tests, docs and readme files from the final result via git archive.
Composer auth is done via a secret mount to avoid layering credentials and keeping the layers lean.
We also run the image with the app
user since doing it as root
is considered a bad practice.
To be able to build this image you need Docker buildkit enabled, this is what
empowers the RUN
mounts and more, check its documentation
here.
# syntax=docker/dockerfile:1.0.0-experimental
# The base target will serve as initial layer for dev and prod images,
# thus all necessary global configurations, extensions and modules
# should be put here
FROM usabillabv/php:7.3-fpm-alpine3.11 AS base
# When composer gets copied we want to make sure it's from the major version 1
FROM composer:1 as composer
# The source target is responsible to prepare the source code by cleaning it and
# installing the necessary dependencies, it's later copied into the production
# target, which then leaves no traces of the build process behind whilst making
# the image lean
FROM base as source
ENV COMPOSER_HOME=/opt/.composer
RUN apk add --no-cache git
COPY --from=composer /usr/bin/composer /usr/bin/composer
WORKDIR /opt/archived
# Mount the current directory at `/opt/project` and run git archive
# hadolint ignore=SC2215
RUN --mount=type=bind,source=./,rw \
mkdir -p /opt/project \
&& git archive --verbose --format tar HEAD | tar -x -C /opt/project
WORKDIR /opt/project
# Mount composer.auth to the project root and composer cache if available
# then install the dependencies
# hadolint ignore=SC2215
RUN --mount=type=secret,id=composer.auth,target=/opt/project/auth.json \
--mount=type=bind,source=.composer/cache,target=/opt/.composer/cache \
composer install --no-interaction --no-progress --no-dev --prefer-dist --classmap-authoritative
# Copy the source from its target and prepare permissions
FROM base as prod
WORKDIR /opt/project
COPY --chown=app:app --from=source /opt/project /opt/project
# Install Xdebug and enable development specific configuration
# also create a volume for the project which will later be mount via run
FROM base AS dev
COPY --chown=app:app --from=composer /usr/bin/composer /usr/bin/composer
RUN docker-php-dev-mode xdebug \
&& docker-php-dev-mode config
VOLUME [ "/opt/project" ]
$ DOCKER_BUILDKIT=1 docker build -t "my-project-dev:latest" \
--target=dev .
You want to run this in your CI/CD environment, you can create the composer.auth
file there, for this example let's
get your computer's file and mount the secret.
$ cp ~/.config/composer/auth.json .composer-auth.json
$ DOCKER_BUILDKIT=1 docker build -t "my-project-prod:latest" \
--target=prod \
--secret id=composer.auth,src=.composer-auth.json
We also have a simple, but fully functional PHP FPM example, check it here.