We recently used the Docker Hub to create a shared infrastructure and make our technology team more streamlined in our approach to collaborative development.
At Forum One, we like finding tools that help us do our jobs more efficiently so that we can deliver impactful digital solutions to our clients. Docker has helped streamline our developers’ work by using containers. If you are unfamiliar with containers, they are a tool that allows a developer to package up their code with all of the necessary parts (such as libraries, we’ll get to that in a minute!) it needs to be effective and ship it all out as one package.
Creating images for Drupal & WordPress
To create our shared infrastructure, we started by building images for Drupal and WordPress. Though Drupal and WordPress already have official images (i.e., library/drupal and library/wordpress), they come with some issues that make them less suitable for our needs. The Docker Library images are designed for rapid startup by people who may not be familiar with Docker, so there are often newbie-oriented customizations that we have to overwrite. For instance, the Drupal 8 image ships with a copy of the bundled source, which we don’t use since our sites are managed with Composer. This results in about 140MB of wasted space. For WordPress, the image will attempt to download a copy if it wasn’t detected, but the script isn’t compatible with the directory structure used by WP Starter, so we have to override the script.
Another requirement for a shared infrastructure is that we have to support multiple PHP versions of a website. But we don’t always deploy containers; for example, for sites that run on platforms such as Pantheon, Acquia, and WP Engine, we deploy files to their Git repositories. The best way to ensure compatibility with local development is to let developers choose the tools that are as close to parity with the production environment as possible.
With these two requirements in mind, our images (forumone/drupal8 and forumone/wordpress) are simple FPM-based images that only include the base packages necessary to run a site that has been installed via some other means (either a bind mount or a Docker COPY instruction).
One important consideration when building Docker images is that they should be specialized to a single purpose. The images we’ve built for serving Drupal 8 and WordPress deliberately exclude the usual tools you’d expect when developing those sites. For Drupal 8, those tools are Drush and/or Drupal Console, and for WordPress it’s WP-CLI. While some of this concern relates to disk space usage, this also has an impact on the security surface area of an image: it’s impossible to exploit a tool that doesn’t exist in an image.
Under the hood, like forumone/drupal8 only includes PHP’s FastCGI process manager (FPM). This tool only serves PHP code via the FastCGI protocol, meaning that there is no room for command-line interaction. Instead, we ship the companion image forumone/drupal8-cli, which includes the command-line PHP interface along with the external tools one expects to use Drush with – SSH commands to log in to other servers, and the MySQL CLI for interactive queries. The forumone/drupal8-cli image doesn’t include an installation of Drush or Drupal Console; these days, those tools are expected to be managed by the project’s composer.json instead. Since these images are augmentations of local development, we simply ask that users bind mount their Drupal 8 installation and invoke Drush commands via an alias that shares SSH credentials with the running container.
The story for WordPress is similar. Instead of installing WP-CLI and its dependencies into the running WordPress site, we have forumone/wordpress-cli. Unlike the Drupal 8 CLI image, we have WP-CLI installed into the image, as it is not expected to be installed inside of a WP site.
Finally, we have a mildly customized Composer image called forumone/composer. The only difference is that our Composer images include the Prestissimo download optimizer by default, resulting in a significant performance gain when downloading and installing new projects.
In order to keep image sizes down, we only install the minimum requirements for Drupal or WordPress. This means that if a project requires another module (such as SOAP or the PECL memcached extension), the user will need to install it in their Dockerfile. In order to simplify this process, we created f1-ext-install, a small binary to perform the package management commands needed to install a PHP extension and its dependencies. The tool has built-in knowledge for extensions we use and can be extended to install other extensions from the environment as well.
This tool has helped reduce a lot of the visual noise of adding extensions to an image and has correspondingly lowered the barrier to entry for adding functionality.
In order to avoid dependencies on the runtime PHP environment (since there are quite a few versions in play), we wrote the tool using the Rust programming language. This has created a very small binary (roughly 2-3MB), meaning that users aren’t paying much extra for the added usability.
The last piece of build tooling we needed was builders for Gesso, the starter theme that we have been using for all of our Drupal and WordPress projects. Gesso has a long history that can be broken down into three eras:
- Gesso 3.x, which uses Gulp and the Node edition of Pattern Lab
- Gesso 2.x, which uses Grunt and the PHP edition of Pattern Lab
- Gesso 1.x and its ancestors f1ux and f1omega (themes that are built using Compass instead of node-sass)
For Gesso 3.x, we’ve released forumone/gesso. This image includes Gulp, Node, and PHP — the tools needed to compile Sass and build Pattern Lab. For Gesso 1.x and the prior iterations, we’ve released forumone/f1ux, which is currently undergoing testing. We named the image f1ux after Gesso’s predecessor to indicate that it’s primarily intended to target old code, not new projects.
While forumone/gesso is built using Dockerfiles, f1ux isn’t. Instead, we’re evaluating using the Nix package manager to build theme-building images. The Nix model breaks a project down into what are called derivations (roughly similar to a package that you’d install with apt-get or brew), and each derivation’s output is stored in an immutable location called the Nix store. When we build f1ux, Nix first ensures that all of the derivations we selected are up to date, and then combines these derivations’ outputs into a Docker image.
We started using this process due to limitations in Dockerfiles and Docker’s layer mechanism. When Docker executes a build, it effectively combines the two steps above – instead of building each element and combining them, it combines each build one at a time. This layering mechanism works well for most projects, especially those that only need a single language (such as Python projects, Node.js-based servers, and so on), but it breaks down due to the order imposed on layers by Docker build.
The Nix store has also provided another benefit: because each derivation has its own store location, we are able to build multiple versions simultaneously without conflicts. We’ve been able to template about 30 different combinations using a little over 400 lines of code – the majority of which are simple declarative build expressions rather than lines of shell script.
Now that we have a working set of baseline images, we are looking to expand the abilities of these images to assist in more development scenarios. Right now, we have a few items lined up.
First, we want to be able to debug our CLI tasks. This involves adding XDebug and researching a way to avoid having to pay the XDebug performance penalty on common operations – we’ve seen sites slow down by a factor of ⅓ when XDebug is activated, and this will only compound when applied to long-running tasks such as DB updates or migrations.
Second, we are looking at potentially expanding our use of Nix to cover the entire Gesso suite. One blocker for the Gesso 2.x and 3.x images is the need to account for differing versions of Node.js. We switched our Sass implementation from Ruby Sass to node-sass in 2.x, which requires either downloading a module compatible with the running version of Node.js or building it from source. If we can build images that target multiple versions of Node.js, we can avoid having to bundle the necessary compilation tools, as well as speed up builds by downloading a cached module.
This can be accomplished more easily using our existing Nix tools to build a matrix of images since we’ve observed that our Buildkite servers often take a long time to build Node.js from source. We can leverage the graph-based caching of Nix to avoid rebuilding Node.js for new Gesso 2.x images.
The most important part of this work, however, is already behind us. Since we have a predictable, standardized starting point for all of our projects, we can improve all of them at once by releasing new images.
Interested in building a similar shared infrastructure?
Get in touch today and our DevOps team would be happy to talk through some ideas with you.