Docker on a diet
Tags: docker, devops, java, python
Published: 2020-05-22 12:05:35


Docker containers are extremely useful when it comes to CI and CD. As a developer, I was amazed with its possibilities and ease of use. What I didn’t know when I started using Docker is that I did it all wrong.

After couple of months building and pushing containers to our private Nexus Registry, our devops approached me and said “Hey, your images took all the disk space of the registry, might want to check if they are a little bit oversized?”.


![The same goes to bloated docker images](https://thepracticaldev.s3.amazonaws.com/i/54m17puu3e9e30et93ou.png)*The same goes to bloated docker images*

I had two docker images, one for Python application and one for Java. Their total size was whopping 6.5Gbs without applications themselves. After all enhancements it’s 1.4 Gb for Python and 660Mb for Java, applications’ binaries included.

In this article I will cover tips and tricks for optimizing docker images size, which will save you registry disk space and (re)deployment time.

## Creating a baseline

First thing to do would be extracting common stuff from your images.

All the common libraries, files should be in a base image, so they would be cached inside docker layers. For my case I moved Apache Thrift and custom log rotation utility (which is build from sources) to baseline image, instantly saving 350Mb worth of space.

## Few words about layers


![Docker images are like ogres. They have layers](https://thepracticaldev.s3.amazonaws.com/i/zb1gy1nyfb5gonoysei7.jpg)*Docker images are like ogres. They have layers*

Each docker image consist of series of layers. These layers are changes made to the image with commands like FROM, RUN, COPY, you can think of it as a commits in a git repository. Docker makes use of union file systems to combine these layers into a single image. Union file systems allow files and directories of separate file systems, known as branches, to be transparently overlaid, forming a single coherent file system.

## Keep an eye for temporary files

Layers come in handy, because they can be reused during build or deploy, but they also can make image significantly bigger.

Imagine creating image for nodejs application:

![](https://cdn-images-1.medium.com/max/2720/1*09XV8yPlj1UzJdqqIyQ0Bg.png)

Looks something like we would write in a bash-script, right? Totally wrong for docker though. Size of our new image is 248Mb. Let’s look into *docker history* command:

![History displays size for each layer](https://thepracticaldev.s3.amazonaws.com/i/1r0uysxl45os9r6n67l1.png)*History displays size for each layer*

Output suggests that we have 67.2Mb of data twice. That’s because we unzipped in one layer and copied in another and *rm* in the last **RUN** command had zero impact on previous layers. So the correct way to optimize this example would be:

![All of our commands combined](https://cdn-images-1.medium.com/max/2820/1*IZAUjm32NhZ13BEj5McVCA.png)*All of our commands combined*

Notice that there is also a *rm -rf /var/lib/apt/lists/* command which will wipe the cache of aptitude. Let’s look at *docker history* again.

![It’s dramatically smaller](https://thepracticaldev.s3.amazonaws.com/i/uirwigyyu0f1qnaa57pn.png)*It’s dramatically smaller*

## Don’t fall into the COPY trap

The **COPY** command is used to put data from host machine inside the image during the build process.

What’s bad about **COPY** is that it creates a separate layer, so the copied data will reside in the image forever. Copying archives or some temporary data is really a bad idea. Prefer wget (or another tool) over COPY if it’s possible or…

## Use Multi-Stage Builds

Multi-stage builds is a relatively new feature requiring Docker 17.05 or higher. This added the ability to create temporary image and then use it’s filesystem during the build of final image. The basic idea is to install all the dependencies and compile all the sources in intermediate image and copy the result to final image. Let’s have a look at the example:

![Here we install apache thrift](https://cdn-images-1.medium.com/max/2540/1*xnICrjW4G-H03nvm6uoGSA.png)*Here we install apache thrift*

Looks like we have a lot of build-time only dependencies:

![Notice apt-get row](https://thepracticaldev.s3.amazonaws.com/i/zx8p2g2otl75kqqk4k0h.png)*Notice apt-get row*

Now we add another build stage and copy binary from the “compiler” image:

![Notice the “--from” argument](https://cdn-images-1.medium.com/max/2524/1*ASO7OktElbQ--wulw7V1rA.png)*Notice the “--from” argument*

Small change and a nice reward:

![That’s 200Mb less](https://thepracticaldev.s3.amazonaws.com/i/urkmer6wo5objgebtjab.png)*That’s 200Mb less*

Be careful though and don’t remove runtime dependencies of a package!

## Java runtime

Have you heard of [jlink](https://docs.oracle.com/javase/9/tools/jlink.htm)? It’s awesome! You can create your own JRE with only modules you need, which presumably will be smaller than any “slim” image. I suggest using [AdoptOpenJDK](https://hub.docker.com/r/adoptopenjdk/openjdk11) image as jlink provider. The idea is to create JRE with jlink and then COPY it using multi-stage build:

![“baseline” image refers to the first section of article](https://cdn-images-1.medium.com/max/3040/1*UG0kZW98QlLZez8PC64VbA.png)*“baseline” image refers to the first section of article*

Let’s look through docker images

    adoptopenjdk/openjdk11 x86_64-ubuntu-jdk-11.28       323MB
    adoptopenjdk/openjdk11 jdk-11.0.5_10-ubuntu-slim     250MB
    adoptopenjdk/openjdk11 x86_64-ubuntu-jre11u-nightly  127MB
    custom-jre-build       jre11                         62.5MB

Using jlink we achieved image two times smaller than the smallest AdoptOpenJDK image. Not to mention the ease of integration between custom JRE and baseline image (a simple COPY instruction).

## Java dependencies

![It really does](https://thepracticaldev.s3.amazonaws.com/i/xk64sdoin5f0tfcd76ty.jpg)*It really does*

Most of the projects use Maven or Gradle for dependency management. And sometimes we have **a lot** of dependencies. In our case it was 400Mb worth of jars! Build tools put all the dependency jars inside the main jar file and given the fact that dependencies are not being update very often, we will have a lot of images with the same dependencies. Looks like like a perfect candidate for moving to another layer!

First, you will need to extract dependencies from you project and if they were updated, push them to your registry.

Let’s see how to do it with Maven and SpringBoot. Add *excludeGroupIds* parameter to your builder plugin configuration:

![pom.xml](https://cdn-images-1.medium.com/max/2080/1*VYp_tXbuAsoem6si4cCnxg.png)*pom.xml*

And use this little bash script:

![](https://cdn-images-1.medium.com/max/2548/1*OnEa3Qq0fIcMbg3wLenyZg.png)

Now, how do we know if dependencies were updated and we need to push them? And how do we use that “dependencies image” as a baseline for application image?

I made this hacky approach where I calculate the md5-hash of pom.xml and try to pull docker image with such hash in version from registry. If it’s not there — create it and push it. This is how I did it:

![Build java dependencies docker image](https://cdn-images-1.medium.com/max/2956/1*DZw7FP0DyLLN9gOAL1HXPQ.png)*Build java dependencies docker image*

![dependencies.dockerfile](https://cdn-images-1.medium.com/max/2000/1*XLHpXhszktwxEaUCmxguyQ.png)*dependencies.dockerfile*

Next, we pass dependencies hash to application’s dockerfile via **ARG** command and use it in version of **FROM-**image:

    docker build --build-arg version=${DEPENDENCIES_HASHES} -f application.dockerfile .

![application.dockerfile](https://cdn-images-1.medium.com/max/2000/1*8qWLDKVb-NK5EZoMVygb3Q.png)*application.dockerfile*

Note, that now there is a maven/gradle plugin [jib](https://github.com/GoogleContainerTools/jib) that does job similar to what I’ve written above (not applicable to our case, though, because our Gitlab runner clear cache after every build).

## Python specific

I used the same approach as for Java container. I calculate the hash of requirements.txt file and use it as a version for intermediate container.

Inside a dockerfile you should use multi-stage build. In first stage install all the build dependencies and then do *pip install --no-cache-dir*. In the result stage copy /opt/miniconda3 dir from previous stage.

![Build python dependencies docker image](https://cdn-images-1.medium.com/max/2000/1*VGKlcNawlhTsO1Y9YLUw2Q.png)*Build python dependencies docker image*

Let’s look at python-dependencies.dockerfile. Don’t mind the apt-get install section, these are just packages that I needed for my python dependencies.

![python-dependencies.dockerfile](https://cdn-images-1.medium.com/max/2000/1*WpoEGKMpHbteO51Nrjj4ww.png)*python-dependencies.dockerfile*

What’s left to do is create the final python image base on dependencies image. I just copy all python scripts (and a bash script for starting python).

![application.dockerfile](https://cdn-images-1.medium.com/max/2000/1*qnIQlao2dJOq8w6ImNq6kw.png)*application.dockerfile*