Composer install in Dockerfile Without Breaking Cache

August 6, 2016

When building a Docker image for your PHP application there are some steps you should take to prevent the cache from being invalidated prematurely. Cache misses can slow down the build process drastically.

Consider the following Dockerfile snippet:

RUN curl -sS https://getcomposer.org/installer | \
    php -- --install-dir=/usr/bin/ --filename=composer
WORKDIR /app
COPY . ./
RUN composer install --no-dev --no-interaction -o

The first instruction is to install Composer. Then we change the working directory to /app which is created if it doesn't already exist.

The COPY instruction will copy your application's source directory to the file system of the container. These files have likely been changed and the COPY instruction will invalidate the cache, which means that all subsequent Dockerfile instructions will run on every build.

Having Composer install all its dependencies on every build is not ideal.

Here's an alternative Dockerfile:

RUN curl -sS https://getcomposer.org/installer | \
    php -- --install-dir=/usr/bin/ --filename=composer
COPY composer.json ./
COPY composer.lock ./
RUN composer install --no-scripts --no-autoloader
COPY . ./
RUN composer dump-autoload --optimize && \
	composer run-script post-install-cmd

Instead of copying all the source files we only copy composer.json and composer.lock. These rarely change and will not invalidate the cache. Neither will the RUN instruction unless you change the command itself.

With these files in place we can run composer install before the cache is invalidated by the new application source files; our dependencies will not be downloaded with every Docker build. However, without the source files we cannot run our Composer scripts or build the autoloader just yet, so we use the --no-scripts and --no-autoloader flags.

Then, only after we have copied all the source files, we dump the autoloader and run the scripts.

The output from docker build will look something like this:

Step 9 : COPY composer.json ./
 ---> Using cache
 ---> b9c0e85272f6
Step 10 : COPY composer.lock ./
 ---> Using cache
 ---> b9e7a481b7ab
Step 11 : RUN composer install --no-scripts --no-autoloader
 ---> Using cache
 ---> 415b3cc08fdf
Step 12 : COPY . ./
 ---> ff26e89ee2d7

Now Docker is reusing the cache for all but the last layer where the source code is copied.

Create a .dockerignore file

Because we're copying the entire source directory into the image, any file change will invalidate the cache, even the change of a log file. We can easily remove certain files and directories from the build context by adding a .dockerignore file which lists all the items we want to exclude, e.g. a log/ directory.

Generally speaking, any file that isn't required by your image should be added to .dockerignore in order to increase build performance.