by Adam Brett

Docker Patterns - The Builder Pattern

When producing docker images there's a desire to keep the images as small as possible. This can be for a number of reasons, including:

  • reducing time to pull the image
  • reducing the potential for bugs introduced by dependencies
  • reducing the potential for exploits introduced by unused tools

To this end, the docker community has created many small images, the most prevalent and successful of these has been the Alpine Linux images that are devoid of all of the cruft that shipped with early ubuntu and debian based images.

Just using these images is not enough however. Take this Dockerfile as an example:

FROM node:4

COPY . /usr/src/app

RUN npm install && npm run build

CMD ['npm', 'run']

This was probably a fairly common Dockerfile before the Builder Pattern. We install our npm dependencies, transpile our ES6 and SCSS, then run our app.

The problem with this is that all of the tools we needed to build that code is now left inside our image. If any of them are ever found to have a vulnerability, our application needs to be re-built and deployed with a new version.

Instead, consider this:

npm install
npm run build
docker run --rm -v $(pwd):/usr/src/app -w /usr/src/app node:4

And then the production docker image:

FROM node:4

COPY ./package.json /usr/src/app
COPY ./build /usr/src/app

RUN npm install

CMD ['npm', 'run']

Here, we're using a script to run the npm commands from our previous image inside a Transient Container and mounting our current working directory as a Dev Volume. You should also consider using a Cache Volume to speed up this process on subsequent runs. This means the result of the build will be cached in the current directory, ready for us to copy it in when building our production image.

This ensures that we only have the code and packages we need to run our application in our production image and all of our development tools and packages are discarded in our Transient Container

 Builder Pattern vs Pre Build

There are a few reasons that we use the Builder Pattern instead of simply building the code on our host before adding the build directory to the image. The main reason is compatibility. You could potentially have a dependency that is dynamically linked or otherwise dependent on the operating system that it was built on. Building inside of the same image we will be running in will eliminate that. The second reason is that you can tie your build to a consistent version of your tooling and won't have to rely on the various versions installed on your build server or other developer's machines.

Docker Compose

As with most things docker based, this can be simplified using docker-compose and the [Tool Entrypoint] pattern:

  image: node:4
  working_dir: ${PWD}
  entrypoint: node

    service: node
  entrypoint: npm

    service: npm

Now you can run your build with:

docker-compose run --rm build

For exclusive content, including screen-casts, videos, and early beta access to my projects, subscribe to my email list below.

I love discussion, but not blog comments. If you want to comment on what's written above, head over to twitter.