The neatest thing I’ve discovered about Docker in the last few months are multi-stage builds. To put it simply a multi-stage build is a Dockerfile with two or more FROM
stanzas. Let’s explore what that means for a PHP project.
First, a goal: the images built for any application should 100% ready to go. That means no composer install
on boot or anything like that. Multi-stage builds are a way to get there, but still have the entire build process contained in a Dockerfile.
Steps to Build a PHP Application
Whether building an image or pushing code to a running server at least two things will happen:
- Push/add the code
- Install dependencies
These two steps could take place outside of a Docker file, then simply add the prepared coded to the image.
# copy PHP code & composer config cp -r src build/src/ cp composer.* build/ cd build && composer install && cd - # build container docker build -f Dockerfile build/
With a multistage docker build, the actual build steps take place inside docker itself. Here’s the actual build part of a Dockerfile for a silly little PHP app. This example is using a simply alpine linux PHP image, we’ll talk more about more complex things below.
FROM php:7.2-alpine AS deps # install composer RUN apk update \ && apk add ca-certificates coreutils \ && wget -q https://getcomposer.org/installer -O composer-setup.php \ && wget -q https://composer.github.io/installer.sha384sum -O installer.sha384sum \ && sha384sum installer.sha384sum \ && php composer-setup.php --install-dir /usr/local/bin # Add our application files here ADD src /app/src ADD bin /app/bin ADD composer.* /app/ WORKDIR /app # Install deps RUN composer.phar install --no-dev --classmap-authoritative
Here we just install and verify composer, add our application files (step one above), and then install dependencies (step two above).
This could be shipped, but it has some fluff. Our final image doesn’t really need coreutils
or composer itself installed. These two little things are not bad, but what about things like compilers and other tooling that may get added to build extensions and such? Should those be included in a final image? Nope.
For now, let’s stick with our simply example. We’ll add another FROM
stanza and copy our final /app
directory from the deps
portion of the build.
FROM php:7.2-alpine AS deps # see above for what's here. # this is the REAL application container now FROM php:7.2-alpine # we don't need to do anything here by copy the `/app` folder from the # `deps` stage above. Its /app folder will have all the vendor files etc COPY --from=deps /app /app CMD ["/app/bin/hello"]
That’s it! The final image will have our nicely built /app
directory but no fluff required to install composer.
The images is smaller, but more importantly, every step of the build process is in one place. This means a complete container is a docker build
away.
The dependencies are installed and classmaps generated all in the same environment as production.
What About Applications that Require Certain PHP Extensions?
The Alpine PHP images are extremely barebones compared to something like PHP packages on Ubuntu. So if additional extensions are required, I’d suggest using a common php
image on which the other application container can be based.
For example, here’s one that installs the pcntl
and sockets
extensions.
FROM php:7.2-alpine RUN docker-php-ext-install pcntl sockets
This can be built with something that tags it with a human understandable name and used as a base for other images.
FROM example-php:latest as deps # install composer, app code, deps, etc here FROM example-php:latest COPY --from=deps /app /app
Building both containers is two steps: one step too many. A script to build both may be in order.
docker build --tag example-php:latest -f path/to/php/Dockerfile docker build --tag example-app:latest .
What About Private Dependencies?
Composer uses a few environment variables, specifically COMPOSER_AUTH
, that make this easy to handle.
COMPOSER_AUTH
expects a JSON string exactly like the JSON found in ~/.composer/config.json
. For example, a COMPOSER_AUTH
value for fetching private dependencies from Github:
COMPOSER_AUTH='{"github-oauth": {"github.com": "YourGHOAuthTokenHere"}}' composer install
Combine that composer feature with docker build arguments and private dependencies are easy to handle.
FROM php:7.2-alpine AS deps ARG COMPOSER_AUTH # install composer # add app code # Install deps RUN composer.phar install --no-dev --classmap-authoritative
Build arguments are available in the environment to running commands, so the RUN composer.phar install
stanza will see and use that COMPOSER_AUTH
value to do its work.
Docker will also, helpfully fail if the build arg is not included. The container can be built like this:
docker build \ --build-arg 'COMPOSER_AUTH={"github-oauth":{"github.com":"YourGHOAuthTokenHere"}}' \ --tag example-app:latest .