In this post, we will see a docker build example of a node js API application starting from slow and ending up in a ~10x faster build. I have already talked about the reasons to use docker for development environment. I have also mentioned how docker changed the way we software engineers work and multi-stage docker build in past posts. For this one let’s focus on the docker build example with faster build in mind.
Information before jumping in #
- Familiarity with Docker and the docker build process is required
- All examples are based on
Docker version 19.03.13, build 4484c46d9d
on a Mac - The Currency API app is used for this docker build example
Why faster docker build #
There are many reasons you would want your Docker containers to build faster, here are some pressing ones:
- It will save the software engineer’s time while waiting for container images to build in the CI/CD pipeline. Imagine this if all your docker build took half the time it would result in a lot less waiting.
- It will also save engineers time to build and run the software locally. In this era of microservices if those images would build faster it would help a lot.
- The faster build also enables faster deployment and releases. If you wanted to rollback a buggy deployment if the build took 10 minutes that buggy code stays in prod for at least those 10 minutes while the reverted change is building.
Docker Build example: slow build #
Let’s look at the docker below, this innocent-looking docker file is taken from a Node Js API. It has one major issue we will uncover as we proceed:
FROM node:18-alpine
WORKDIR /src
COPY . /src
ENV NODE_ENV=production
RUN npm install --production
EXPOSE 8080
CMD ["node", "index.js"]
RUN npm ci
is another better option in place of RUN npm install --production
. If you want to use something else in place of Node.js it would be good to explore a couple of Node.js alternatives.
Let's use the regular docker build #
When we try to build the above docker file with docker build using the following command
time docker build -t node-14-first-bad-cache-no-buildkit .
The time
command is prefixed to the docker build
command so that we know the time it takes for the docker build command to finish. Below is how long it took:
As seen above it took 57.17 seconds.
Easy speed up, use BUILDKIT #
Docker build has recently added BUILDKIT from version 18.09. Docker basically says it is an overhaul of the build process. As mentioned in this post it is faster, efficient, and concurrent. You can read more about its goodness in this article on docker.com. For now, let’s see it in action:
time DOCKER_BUILDKIT=1 docker build -t node-14-second-bad-cache-with-buildkit .
As you can see the build time is less than half of the previous build without buildkit.
This build only took 27.32 seconds compared to the above build which took 57.14 seconds.
Docker Build example: fast build #
Ok, there is a major issue in our previous docker file. The docker cache is busted on each change be it our custom code or any other npm modules being added. Read more about docker build cache in this post too.
Faster docker build with proper caching #
Our code changes almost every time but the npm modules we pull in change infrequently. So we can safely cache the npm modules as below:
FROM node:18-alpine
WORKDIR /src
COPY package.json package-lock.json /src/
ENV NODE_ENV=production
RUN npm install --production
COPY . /src
EXPOSE 8080
CMD ["node", "index.js"]
You can have a look at the diff between these two docker files here. The main change is that we copy the package.json and package-lock.json file first then run npm install. Only after that, the custom code is copied to /src
. So if you don't add a new npm library the cache will hold up.
It took 34 seconds to build for the first time as below with the following command:
time DOCKER_BUILDKIT=1 docker build -t node-14-third-good-cache-with-buildkit .
Is docker build fast after code change? #
For this docker build example, I added a line of comment in the index.js file of the Node JS API application. Now let’s see how long it takes and if it caches the node_modules used in the npm install
command.
time DOCKER_BUILDKIT=1 docker build -t node-14-fourth-good-cache-file-change-with-buildkit .
The build took only 6.01 seconds, thanks to great cache usage by docker and use of buildkit.
Even though the code changed but the NPM modules were cached making the build complete in mere 6 seconds. The same principles apply for exploiting docker build cache. It can be applied to PHP with composer.json and composer.lock file or any other language. Always think of the previous command run and how can it be cached better.
All four images were around 233 MB, one took ~60 seconds and the last one took 6 seconds. That is like 10x faster.
Conclusion #
If you are building docker images don’t forget to use BUILDKIT, it is super efficient. On top of BUILDKIT always analyze how to exploit docker build cache to your advantage of faster docker builds.
I hope this small docker build example has helped you. Things like having smaller docker images like using alpine base Image can also help a bit in speeding up your docker build.