1. Overview
The Docker build context consists of files or folders located at the particular path or URL. During the build, those files are sent to the Docker daemon so that the image can use them as files.
In this tutorial, we’ll learn about the Docker build context and issues related to it. We’ll explore various ways to reduce the build context for building an image.
2. Understanding the Docker Build Context
Before we move forward with the issue, let’s first understand the Docker build context and how it works. The Docker build context is the collection of files and directories that will be accessible to the Docker engine when we run docker build, and anything that is not part of the build context will not be accessible to commands in our Dockerfile.
In some cases, the Docker CLI and Docker Engine might not be running on the same machine. During docker build, the Docker CLI sends the files to the Docker engine for building the image.
We can also use the URL of the GIT repository as the build context. As a result, the build context becomes the content of the specified git repository.
3. Understanding the Problem
We’ll understand the problems related to the build context while building an image. To further understand the issue of build context, let’s take an example and create a sample Dockerfile:
FROM centos:7
MAINTAINER [email protected]
COPY jdk-8u202-linux-x64.rpm /
To build the image, we need to run the following command:
$ docker build -t javaapplication .
The output of the above command:
Sending build context to Docker daemon 178.4MB
Step 1/3 : FROM centos:7
---> eeb6ee3f44bd
Step 2/3 : MAINTAINER [email protected]
---> Using cache
---> 4c798858cf11
Step 3/3 : COPY jdk-8u202-linux-x64.rpm /
---> Using cache
---> 9c58f775bb80
Successfully built 9c58f775bb80
Successfully tagged test:latest
Let’s now put jdk-8u202-linux-x64.tar.gz of size 186M into the same directory without making changes in the Dockerfile.
On running the docker build command, we’ll get the following output:
Sending build context to Docker daemon 372.5MB
Step 1/3 : FROM centos:7
---> eeb6ee3f44bd
Step 2/3 : MAINTAINER [email protected]
---> Using cache
---> 4c798858cf11
Step 3/3 : COPY jdk-8u202-linux-x64.rpm /
---> Using cache
---> 9c58f775bb80
Successfully built 9c58f775bb80
Successfully tagged test:latest
Here we can see that the build context to Docker daemon increased from 178.4MB to 372.5MB, although we didn’t make any changes in the Dockerfile.
One of the key points of the Docker build context is that it includes all the files and folders of the current working directory recursively and sends them to the Docker daemon.
4. Solution Using the .dockerignore File
In order to reduce the Docker build context. We can use the .dockerignore file to solve the build context issue.
The Docker CLI searches for a file named .dockerignore in the context’s root directory before sending it to the Docker daemon. In case the file exists, the CLI excludes directories and files that match its patterns. This will prevent large files or sensitive directories from being sent unnecessarily to the Docker daemon.
Here, we’ll add the jdk-8u202-linux-x64.tar.gz file into the .dockerignore file in order to ignore it while creating the build:
$ echo "jdk-8u202-linux-x64.tar.gz" > .dockerignore
Let’s now build the Docker image:
$ docker build -t baeldung .
Sending build context to Docker daemon 178.4MB
Step 1/3 : FROM centos:7
---> eeb6ee3f44bd
Here, we can clearly see that the Docker build context sent to the Docker daemon has been reduced from 372.5MB to 178.4MB.
5. Using EOF File Creation
We’ll create a Dockerfile directly using the docker build command with EOF.
Let’s assume the following Dockerfile:
FROM centos:7
MAINTAINER [email protected]
RUN echo "Welcome to Bealdung"
To build the image for the above Dockerfile. We run the docker build command and get the following output:
$ docker build -t baeldung .
Sending build context to Docker daemon 372.5MB
Step 1/3 : FROM centos:7
---> eeb6ee3f44bd
Step 2/3 : MAINTAINER [email protected]
---> Using cache
---> a7088e6a3e53
Step 3/3 : RUN echo "Welcome to Bealdung"
---> Using cache
---> 1fc84e62de75
Successfully built 1fc84e62de75
Successfully tagged baeldung:latest
Here, we can see that the Docker build context sent to the Docker daemon is 372.5MB. If we run the same Dockerfile using the following way:
$ docker build -t test -<<EOF
FROM centos:7
MAINTAINER [email protected]
RUN echo "Welcome to Bealdung"
EOF
The output of the above command is as follows:
Sending build context to Docker daemon 2.048kB
Step 1/3 : FROM centos:7
---> eeb6ee3f44bd
Step 2/3 : MAINTAINER [email protected]
---> Using cache
---> a7088e6a3e53
Step 3/3 : RUN echo "Welcome to Bealdung"
---> Running in 6240c486fcb5
Welcome to Bealdung
Removing intermediate container 6240c486fcb5
---> 1fc84e62de75
Successfully built 1fc84e62de75
Successfully tagged baeldung:latest
Here, we can see that the build context has been reduced from 372 MB to 2.048KB. This solution works only for those cases where we are not using any COPY and ADD commands.
6. Optimizing the Docker Images
Let’s now discuss various ways to optimize an existing Docker image. The most challenging aspect of developing images is keeping the image size as small as possible. Every instruction in the Dockerfile adds a layer to the image, and we should try to remove any unwanted artifacts before moving on to the next layer.
To write a really efficient Dockerfile, we have traditionally needed to employ shell tricks and other logic to keep the layers as small as possible and to ensure that each layer has the artifacts it needs from the previous layer and nothing else.
6.1. Using Multi-Stage Builds
In multi-stage builds, we use multiple FROM statements in a single Dockerfile. Every FROM instruction uses a different base, and every FROM instruction starts a new stage of the build. The Artefacts can be copied selectively from one stage to another.
Let’s look into an example to use multi-stage in a Dockerfile:
FROM centos:7 as builder
RUN yum -y install maven
COPY spring-boot-application /spring-boot-application
WORKDIR /spring-boot-application/
RUN mvn clean package -Dmaven.test.skip=true
FROM centos:7
RUN yum install -y epel-release \
&& yum install -y maven wget \
&& yum -y install java-1.8.0-openjdk \
&& yum clean all
COPY --from=builder /spring-boot-application/target/spring-boot-application-0.0.1-SNAPSHOT.jar /
CMD ["java -jar ","-c","/spring-boot-application-0.0.1-SNAPSHOT.jar && tail -f /dev/null"]
Here, in the above Dockerfile, we used two From statements. First, we built the spring-boot-application-0.0.1-SNAPSHOT.jar using the mvn command in the first layer. Then we used the same jar file in the second layer using the “COPY –from=builder” command. This way we can remove the first layer and reduce the size significantly.
6.2. Reduce the Number of Stages
When building an image, we should pay attention to the layers created by the Dockerfile. The RUN command creates a new layer for each execution. The image size can be reduced by combining the layers.
Generally, users run the command like this:
RUN apt-get -y update
RUN apt-get install -y python
The above Dockerfile will create two layers. But combining both the commands together will create a single layer in the final image:
RUN apt-get -y update && apt-get install -y python
So, smart combinations of commands can lead to smaller images.
6.3. Build Custom Base Images
Docker caches images. Whenever we have multiple instances of the same layer, we should optimize the layers and create a custom base image. The loading times will be sped up, and tracking will be easier.
7. Conclusion
In this tutorial, we learned about the concept of the build context in Docker. We first discussed the detail and execution of the Docker build context.
We then addressed the issue of increased build context while creating a Docker image using the docker build command. First, we explored the use cases of the problem and then solved them using different ways in the Docker.
Finally, we looked into different approaches to optimize a Docker image.