Few months ago I came with an idea to build Composer’s Docker images, but containing only binary file. Yesterday my pull request was merged, and now it’s possible to use such images in your own Docker builds as the simplest way to get Composer binary in your own image! 😁

Explanation

If you want to include Composer in your Docker builds, you have several options for that. But previously if you wanted to use COPY --from=composer approach, you had to download ~190MB image of Composer just to pull out one ~2.5MB file from it. Obviously, it wasn’t optimal, even when we consider Docker build cache.

After my changes, every Composer’s Docker image will have binary-only equivalent. There are 3 differences though:

  • composer/composer image must be used (details below)
  • -bin suffix has to be added
  • Composer binary is located in the root directory (/composer), unlike in the full image (/usr/bin/composer)

For example, if you want to install the latest release from v2 branch, you need:

FROM php:8-alpine

COPY --from=composer/composer:2-bin /composer /usr/bin/composer

When to use it (or not)

Binary-only images are useful only when you build your own PHP-based images, and you want to install Composer there too. Instead of installing it programmatically, you can use Docker image and extract ready-to-use binary file from it.

Having said that, it seems obvious that binary-only images are not suitable to run anything using them. Running docker run -it --rm composer/composer:2-bin <anything> won’t work because those images do not contain anything more than Composer’s binary - there is no PHP runtime or shell.

Implementation details

In mentioned Pull Request there are 2 kind of changes:

  • build targets were defined in Dockerfiles in order to be able to build both type of images
  • GitHub Actions were modified, so every pipeline contains Docker build both for regular and binary-only images, which are tagged respectively

The first change is more interesting, so let’s look at the changes for 2.4 branch:

diff --git a/2.4/Dockerfile b/2.4/Dockerfile
index c0b4ca7..866be1c 100644
--- a/2.4/Dockerfile
+++ b/2.4/Dockerfile
@@ -1,4 +1,4 @@
-FROM php:8-alpine
+FROM php:8-alpine AS binary-with-runtime
 
 RUN set -eux ; \
   apk add --no-cache --virtual .composer-rundeps \
@@ -89,3 +89,10 @@ WORKDIR /app
 ENTRYPOINT ["/docker-entrypoint.sh"]
 
 CMD ["composer"]
+
+FROM scratch AS standalone-binary
+
+COPY --from=binary-with-runtime /usr/bin/composer /composer
+
+# This is defined as last target to be backward compatible with build without explicit --target option
+FROM binary-with-runtime AS default

First of all, binary-with-runtime name was added to existing build, which is then used in two ways: for building full image (docker build --target binary-with-runtime), and as a base for binary-only image (COPY --from=binary-with-runtime /usr/bin/composer /composer). The latter is called multi-stage build, and it’s a way to split build into many independent stages, from which some files can be copied to other layers. It’s helpful especially for optimising final image size, because e.g. any temporary files can be easily omitted.

The last line is interesting though - it ensures backward compatibility by aliasing binary-with-runtime with default stage. Since this is last stage in the file, it will be used when --target is not defined in build command.

Summary

It is small, but really important and helpful change, that can optimise many existing, real-world pipelines. Try it out in your build and let me know what you think 🙂