Creating optimised Docker Images using Multi-Stage Builds

Have you ever wondered how you can leverage building docker containers in your project to take your experience to the next level? If so then have a look at this step by step guide that hopefully will help you achieve that goal using multi-stage builds.

If you would like to preview the code used in this article, just clone this repository.

Why use Multi-Stage builds?

In a nutshell multi-stage builds feature is a recent improvement to the docker build system, which improves and simplifies control over:

  • final image sizes
  • image build orchestration

The refinement is achieved via allowing to consecutively use different images (for whatever purpose is needed) and pipe data subsequently through them within a single Dockerfile with assumption that the final image is built based on the last FROM statement.

Standard approach

If you already worked with a docker, you certainly faced the problem of large built images or had to do custom shell scripts to orchestrate build. Each instruction in the dockerfile adds another layer to the image which increases its size. Moreover, creating more complex images may require the use of additional shell scripts and multiple dockerfiles which may be cumbersome to manage.

To illustrate the above issues, I’ve created a simple java web application that I am going to build and run in a docker container. By default, without using multistage builds, the dockerfile would look like this:

WORKDIR /deployments
COPY . /deployments
RUN mvn package
CMD ["java", "-jar", "target/demo-webapp-0.0.1-SNAPSHOT.jar"]

In the above file we perform following steps:

  1. Create a new image based on the redhat’s universal base image that already contains openjdk-11.
  2. Create the /deployments working directory and then copy the content of our project to it.
  3. Build java archive file with maven
  4. Run java application from command line

Since we already have the Dockerfile prepared, we can start building the image with the following command:

$ docker build -t demo-webapp:singlestage -f Dockerfile.singlestage .

Afterwards we can check the result:

$ docker images

demo-webapp		singlestage	491de71ef224		24 seconds ago		549MB

Build an image using builder pattern & shell script

The newly created image that runs our application is quite large, so let’s optimize it using the builder pattern. For this purpose, we need to create 2 separate dockerfiles. First responsible for building the application:

# Dockerfile.builder
WORKDIR /deployments
COPY . /deployments
RUN mvn package

Second, as follows, used for launching the app. Separation allows to package only elements that are important to run the app and omitt anything which used just for building.

# Dockerfile.runner
COPY ./deployments/target/demo-webapp-0.0.1-SNAPSHOT.jar .
CMD ["demo-webapp-0.0.1-SNAPSHOT.jar"]

To automate the process of building images defined in separate dockerfiles, we create a shell script that carries out the steps necessary to run our application.

echo "Building demo-webapp builder image..."
docker build -t demo-webapp:singlestage-builder -f Dockerfile.builder .
echo "Extracting builder content..."
docker container create --name extract-builder demo-webapp:singlestage-builder
docker container cp extract-builder:/deployments .
docker container rm -f extract-builder
echo "Building demo-webapp runner image..."
docker build -t demo-webapp:singlestage-runner -f Dockerfile.runner .

After executing the script, we can notice that 2 images were created and the runner image is much smaller than before. This obviously is an improvement in context of distribution and storage.

demo-webapp		singlestage-runner	5fd438ae9faa	9 seconds ago		217MB
demo-webapp		singlestage-builder	f6e90caab67b	10 seconds ago		532MB

Multi-stage build

Along with the implementation of version 17.05, docker provided the multi-stage build functionality which allows you to divide the creation of the container into several phases and build it as several separate images that have access to each other. To take advantage of multistage builds, we need to create a new dockerfile.

# Dockerfile.multistage
FROM AS builder
WORKDIR /deployments
COPY . /deployments
RUN mvn package
FROM AS runner
COPY --from=builder /deployments/target/demo-webapp-0.0.1-SNAPSHOT.jar .
CMD ["demo-webapp-0.0.1-SNAPSHOT.jar"]

As of now dockerfile contains two FROM clauses and it will build 2 separate images consecutively - the first builder and the second, runner.

When building the builder image we execute almost exactly the same commands as in the case of executing the build using the single-stage build. The difference is the application startup command, which will be invoked by the runner image. So let’s try to run it.

$ docker build -t demo-webapp:multistage -f Dockerfile.multistage .

After calling the docker images command, we can see that also this time two images were created. However, we didn’t need to write additional shell scripts for this.

demo-webapp		multistage	44af0173ce49		8 seconds ago		217MB
<none> 		<none>			4969b156508a		8 seconds ago		549MB


As you can see, the multi-stage approach has many advantages:

  • Easier management of the image creation process (thanks to the possibility of including instructions for several related images in a single dockerfile)
  • No need to create shell scripts to automate the entire process.
  • Saving disk space (By using the builder container as a common base for subsequent containers)
  • Reducing the size of the final runtime container

That’s it! We hope you found “Creating optimised Docker Images using Multi-Stage Builds” helpful in your devops activities.

If this topic interests you, visit use multi-stage builds for more details or even better talk to our team today.

Software Engineer @ Iterative Engineering