Speed up your builds with Docker Cache

Cover image

When you start with Docker, it seems normal to get inspired by some samples and examples you grabed around the web.

First steps with Docker

Let’s start, for example, with a simple nodejs project. You grab this nice Dockerfile, and, yes, it works great.

FROM mhart/alpine-node

WORKDIR /src
# Copy your code in the docker image
COPY . /src
# Install your project dependencies
RUN npm install
# Expose the port 3000
EXPOSE 3000
# Set the default command to run when a container starts
CMD ["npm", "start"]

Building again and again but no gain

Now, you are working on your source code. And each time you want to build your image to test your code, you have to wait…

Step 1 : FROM mhart/alpine-node
 — -> cdbaa8672a25
Step 2 : WORKDIR /src
 — -> Using cache
 — -> b45c7e778213
Step 3 : COPY . /src
 — -> efdeb7ccd271
Removing intermediate container 8658a7345ce0
Step 4 : RUN npm install
 — -> Running in f48b8e022460
Removing intermediate container f48b8e022460
Step 5 : EXPOSE 3000
 ---> Running in 60bccf4e876e
 ---> d3591a082b67
Removing intermediate container 60bccf4e876e
Step 6 : CMD npm start
 ---> Running in 9f130513aef6
 ---> 627434bc0bd3
Removing intermediate container 9f130513aef6
Successfully built 627434bc0bd3

Each build took over a minute on my MacBook Pro… and 90% of this minute is dedicated to the step 4, the npm install.

Know docker

Here comes three things to know :

  • Docker build your image step by step.
  • Each step is dependent to the previous one.
  • Each step is cached by Docker

Because my code have changed, Docker use his cache before the step 3. After this, docker rebuild intermediates images from step 3 to step 6.

We can see the “Removing intermediate container” logs : docker don’t use his cache for this steps because it depends on the step 3.

Optimize your Dockerfile to get the best of cache

To get the best of Docker cache :

Think your Dockerfile like layers of images storted by specialization.

  • First, i need a node ready image
  • Second, i need a node image which working dir is /src
  • Then, i need a node ready image which working dir is /src and listening on port 3000.
  • Then, i need a node ready image which working dir is /src and listening on port 3000 and running npm start as default command.
  • I need this previous image but with the node dependencies i specified in my package.json
  • Last step is the most specified step of this image, i want my current source code on it

Here is it ! An optimized Dockerfile of my project :

FROM mhart/alpine-node:5.6.0
WORKDIR /src
# Expose the port 3000
EXPOSE 3000
# Set the default command to run when a container starts
CMD ["npm", "start"]
# Install app dependencies
COPY package.json /src
RUN npm install
# Copy your code in the docker image
COPY . /src

Built it again and again after making some changes to our code : Step 1 : FROM mhart/alpine-node — -> cdbaa8672a25 Step 2 : WORKDIR /src — -> Using cache — -> b45c7e778213 Step 3 : EXPOSE 3000 — -> Using cache — -> 02992be285db Step 4 : CMD npm start — -> Using cache — -> 4e7f1d140eb1 Step 5 : COPY package.json /src — -> Using cache — -> faa22ea65977 Step 6 : RUN npm install — -> Using cache — -> ea3b2c32a045 Step 7 : COPY . /src — -> 45c61b45bb99 Removing intermediate container 871632345f49 Successfully built 45c61b45bb99

Rebuilding this image take only 2 seconds now, is far better than over a minute !!!

We can see the “Using cache” logs.

Steps 1 to 4 are always the same in our project, so cache is always used.

The long npm install task at step 6 is only ran if there is a change on copied package.json file on step 5.

The step 7 is the only step where an image is rebuilt based on previous steps when we make a change on our source code.

We hope this post will help you gain time on your future Docker ready projects.