Optimizing My Alpine Docker Image for Use With Drupal and PHP

Callback Insanity
9 min readDec 31, 2019

Editors’ note: This is a story about Docker. There is no weight loss involved. And there are no magic miracle tricks. It’s all a grind. We just had to make the title catchy. Also as this story progresses it will become more and more technical. By continuing you agree not to hold the writing staff liable for any impending seizures or any other legalese.

If I had to describe Docker in one sentence: it’s a format for packaging, shipping, and running your applications from anywhere. Here’s a good intro article at ZDNet. Also about PHP. Here’s an opinionated PHP article. Or, for the visually more oriented, a video. Gee, thanks Google.

So anyway, I created this collection of Docker images for developing PHP applications. Image Docker images are like a zip file for your applications. A very simple web application will need about 5 different of these Docker images to run containers from. For example:

  • Docker Image #1: Web Server (Nginx, Apache)
  • … Image #2: Backend Layer (Node.js, PHP, Java)
  • … Image #3: Database Layer (MySQL, MongoDB)
  • … Image #4: Cache Layer
  • … Image #5: Tooling, Command Line clients, etc.

Imagine each Docker image in this example weighted 20 megabytes. To deploy an entire application, the user would need to download 100 megabytes. Considering today’s internet speeds, that’s not bad at all.

I am trying for my own Docker stack to also be no more than 100 megabytes, so that way it’s easy to download and use.

There’s one problem, though. One of my images currently weights 103 megabytes by itself — just one image!

Here it is, it’s called php-fpm.dev :

alexanderallen/php-fpm.core = 103MB !!!

Now, if you see the nginx image at the bottom of that list, it measures 21.7 megabytes.That’s kinda more of the goal here.

In this story, my goal will be to prune, optimize, or otherwise put one of my own Docker image through a weight loss regimen, ‘till it reaches the goal of ~ 20 megabytes in size.

A Quick Story about a Docker Image That Went On A Diet

If you want to know what Alpine Linux is, go read this concise 2017 blog post by Nick Janetakis about why Alpine is smaller, faster, and safer. Or, this one.

On this 2 year old article using Alpine 3.7, Jérôme Gamez went from using a Debian Docker image that weighted 390MB to trying the official Alpine PHP image coming in at 56.8MB.

Not satisfied with his size reductions, he built his own custom Docker image using Alpine Linux. Jérôme’s custom Docker image came ended up measuring 22.9 Megabytes !

Jérôme went from using 400 to 25 megabytes of space by switching to Alpine Linux — this must be da wae.

Mando Says ’Tis The Way

The Docker Alpine Way

As I mentioned at the beginning of my story, my custom Docker image is fat. A whole 127 megabytes fat:

docker imagesREPOSITORY    TAG     IMAGE ID      CREATED             SIZE
php-fpm.dev latest 54368d1c3a45 3 hours ago 127MB

For comparison, when I started writing this story here’s how my actual list of Docker images looked like:

That’s a long list! More on that in a mintue …

Or, a more recent and readable compressed list, after doing some pruning with docker system prune :

You see,

  • The base Alpine image from which the rest are built is 5.59MB.
  • Alpine-based memcached weights a paltry 9.09MB. This is a community image.
  • My Nginx image is coming in at 21.7MB, pretty decent I’d say.
  • And MySQL itself, a whole friggin’ database, is just 39.1MB.
  • Image with the ID 54368d1c3a45 is my PHP Alpine image before I started optimizing it, it’s been replaced now by image e3e8c8dc05e5.
  • Image with the ID e3e8c8dc05e5 is a barebones Alpine image with just PHP installed, give or take 3–5 PHP extensions (28.7MB)

So there’s no reason for the PHP image to be 127MB.

The thing is, I’m building this custom Docker image for running Drupal. So this isn’t a journey about just making a small image, but rather making the smallest possible image to use with Drupal core. Maybe I should have said that earlier.

Gathering Requirements: Things That Drupal Core Needs You To Install

Here’s the original Dockerfile we’re starting from.

And here’s the minimal requirements we must attain for working with Drupal 8.

The required list of PHP extensions needed for Drupal 8 can be found in Drupal’s composer.json .

Here’s a screenshot so you have an idea:

The PHP language extension that need to be installed in the operating system are denoted by the prefixext- .

These are the lines on the original source code for Drupal’s Composer manifest:

https://git.drupalcode.org/project/drupal/blob/8.7.x/core/composer.json#L7-19

So that’s a pretty basic list, I must say. There’s no way that thirteen PHP extensions are gonna cost us 100+ megs of space. Or, are they? There’s only one way to find out.

Searching for Requirements in Alpine Linux Package Repository

Just like most Linux distributions, when working with Alpine there is a package repository that you can go to find and download precompiled dependencies for your system.

Here is the Alpine Linux package repository.

The alternative to installing precompiled is downloading the sources and compiling them yourself. But I’m not covering that in this story to keep things simple.

Here’s my list of Drupal Core requirements, cross-referenced to the Alpine package repository:

  1. ext-date, bundled probably
  2. ext-dom, package here
  3. ext-filter, no match, probably bundled*
  4. ext-gd, alpine package, required for imaging
  5. ext-hash, no match found
  6. ext-json, alpine package
  7. ext-pcre, no match
  8. ext-PDO, Alpine packages here and here
  9. ext-session, package here
  10. ext-simplexml, package here
  11. ext-SPL, stands for Standard PHP Library, probably bundled
  12. ext-tokenizer, package here
  13. ext-xml, package here

So out of 13 depenendices required by composer.json , at least 9 are available as Alpine packages. PDO — PHP Data Objects appears as only one dependency in composer.json , but it’s really at least two extensions. The base PDO extension, and the whatever database connector you’re using (MySQL in this case).

Here’s a final pseudo-list of Alpine packages available for use with PHP. Note: All packages are prepended by php7- . So the dom package in Alpine would be named php7-dom :

  1. dom
  2. gd
  3. json
  4. pdo #1
  5. pdo #2
  6. session
  7. simplexml
  8. tokenizer
  9. xml

There’s also the extensions not listed by composer.json but listed by Drupal.org:

  1. mbstring: Drupal.org’s wording does not seem to emphasize it’s required. But it’s probably strongly suggested for dealing with UTF-8 and international character encodings. Skipping for now.
  2. openssl is listed as optional. On a production environment skipping SSL would be criminal and reckless. Not making a production application so, skipping for Science!
  3. curl (#10): required

Then there’s the obvious things, such as the PHP language interpreter itself and the PHP service that passes requests to the language interpreter:

  1. PHP language interpreter (#11)
  2. PHP-FPM service (#12)

So in total — we have 12 system packages to install in our optimized Alpine Linux image for Docker.

Installing Requirements

Let’s build and see. You can see in this Dockerfile screenshot that I’ve commented out some of the other packages while doing my research on what to install (ctype, imagic, intl). But if you count them you’ll see that there’s 12 Alpine packages that are going to be installed by the command apk add --no cache . The --no-cache parameter is there because we’re trying to save size here, and normally the apk command caches the data of the package repositories available. We don’t the package repo cache and can live without it. Also if you note, the line${PHP_VERSION} will be counted as a package. The value of ${PHP_VERSION} is php7 , and that’s the language processor called PHP that Drupal runs on.

Commented out all that bloat.

This is how my build command looks like:

docker-compose -f build/php-fpm/docker-compose.yml build php-fpm.core

And these are the results:

24 Megs — We’ve made the goal !!

In the third line from the top-left column, you will see the words 24MB cd /tmp && apk add --no-cache ${PHP_VERSION} ... . This is what our Dockerfile is doing. It’s adding all those 12 Alpine packages. And the resulting Docker image layer is 24 megabytes large. Plus the 5.6MB from the Alpine Docker image itself that we’re building onto, and 24.1kb of additional metadata that we’re adding after that.

Comparison

For the sake of science, let’s docker pull kreait’s kreait/php:7.1 Docker image and do a size by size comparison:

Pulling kreait’s image with “docker pull kreait/php:7.1”

Side by side comparison, featuring a Teladi “space reptile” from X4 Foundations merrily sitting in my desktop. Teladi are very generous with adding extra sss’s to words — so his satisfied face has got to do something with profit$$$:

Screenshot #1: The upper console is kreait’s Docker image (63MB), the bottom is mine (24MB)

Screenshot #2: A closer look — you can see the Docker image layer contents on the right columns. Additions are in Yellow.

Screenshot #3: Top-to-Bottom Comparison using “docker images”. Top image (php-fpm.core, RED) is mine. KREIT’S image is GREEN (kreait/php).

The Results:

  • Kreit’s original image: was 22.9 MB, but it’s gained a little bit of weight since.
  • Kreit’s recently pulled image: 69.2 MB. This is what’s showing up in docker pull kreait/php .
  • My PHP image: 29.5 MB.

Given how large the original Docker image I started with was, I had some minor reservations about actually being able to bring the image down to the 20–30 MB range. But I’ve done it! What a belated Christmas present!

This is only the first leg of the optimizing process

That’s just the optimizing part of the weight loss story. And just the initial optimizing at that. I’m sure that going thru the process of actually installing Drupal will require more modules, once you factor all the contrib modules that come with your typical Drupal distribution nowadays (such as Acquia’s Lightning).

You see, optimizing the Docker image for size and eliminating bloat does not necessarily mean it will work. At least on the first try. It could, but it’s not something to be honest I’m expecting. Which is why I wanted to spend the time to write this story — to capture for you the process of optimizing Docker images, and to give you a window into my little fiefdom of DevOps — and the little tiny details that make me tick.

Note: Drupal’s modules are not to be confused with PHP’s native modules — the ones installed as system dependencies. They are separate and written in PHP, as opposed to C or other system-level language. Installing additional Drupal modules (written in PHP) might require installing additional system modules (written in C, C++, etc. and compiled into a PHP module).

The next step of the story is actually taking the new Docker image for a ride and attempting a Docker installation.

Conclusion

Here’s some of the things I covered:

  • Adding Drupal 8 Core minimum requirements to Docker.
  • How to find and install Alpine Docker packages.
  • Reducing the size of your Docker stack by using Alpine in combination with optimization tools such as wagoodman/dive: take a closer look at what your building and shipping!

And it is with this story posted today December 31st, 2019 that I conclude this year, and this decade. May the next year and decade bring good things to you.

I hope that you enjoyed it and if you liked it make sure to follow me and clap!

HAPPY NEW YEAR

--

--

Callback Insanity

Organic, fair-sourced DevOps and Full-Stack things. This is a BYOB Establishment — Bring Your Own hipster Beard.