How to Decrease the Size of Docker Images?

Sunday, January 19, 2020 • edited Sunday, February 23, 2020 • 8 minutes to read

Creating new docker images based on some official Linux distributions or modifying existing ones increase their size. Sometimes, the difference between the new and the old ones dramatically exceeds the size of new packages or modifications. The reason behind that is mostly the way how images structure looks like and how Docker uses images and containers.

How are docker images created?

Docker images consist of multiple layers. Each layer corresponds to separate instruction in image Dockerfile. Additionally, Docker uses a copy-on-write strategy for its images and containers. Let’s look at CentOS official image recipe for an example.

FROM scratch
ADD CentOS-8-Container-8.1.1911-20200113.3-layer.x86_64.tar.xz  /

LABEL org.label-schema.schema-version="1.0" \
    org.label-schema.name="CentOS Base Image" \
    org.label-schema.vendor="CentOS" \
    org.label-schema.license="GPLv2" \
    org.label-schema.build-date="20200114" \
    org.opencontainers.image.title="CentOS Base Image" \
    org.opencontainers.image.vendor="CentOS" \
    org.opencontainers.image.licenses="GPL-2.0-only" \
    org.opencontainers.image.created="2020-01-14 00:00:00-08:00"

CMD ["/bin/bash"]

As you see, it has three instructions (ADD, LABEL & CMD), and all of them create a separate layer, which we can confirm using docker history command on centos:centos8 image. Please bear in mind that FROM scratch means there is no parent image, and there would be no inherited instructions.

$ sudo docker history centos:centos8
IMAGE               CREATED             CREATED BY                                      SIZE                COMMENT
470671670cac        41 hours ago        /bin/sh -c #(nop)  CMD ["/bin/bash"]            0 B
<missing>           41 hours ago        /bin/sh -c #(nop)  LABEL org.label-schema....   0 B
<missing>           4 days ago          /bin/sh -c #(nop) ADD file:aa54047c80ba300...   237 MB

Most instructions create a layer that increases size. Some of them never actually modify any file on disk and are, in general, just instructions for Docker itself. I’d not try to describe in detail how the image structure looks like on disk after it has been pulled from the repository and extracted. However, it would be clear enough if we imagine it as a separate folder per layer with actual files.

Copy-on-write strategy, typical not only for Docker, means that in our case, the new layer doesn’t need to be a copy of the previous one but has in it only changes since the last instruction. It includes all the new, modified, and deleted files. The more changes we do, the bigger the layer is.

A typical way of writing a Docker image

Consider the following Dockerfile listing.

FROM centos:centos8

RUN dnf -q -y module reset php
RUN dnf -q -y module enable php:7.3
RUN dnf -q -y install epel-release
RUN dnf -q -y upgrade
RUN dnf -q -y install php-cli php-json php-mbstring php-mysqlnd php-pdo php-xml
RUN dnf -q -y clean all
RUN curl -sL -o /root/installer.php https://getcomposer.org/installer
RUN php /root/installer.php --install-dir=/usr/local/bin --filename=composer
RUN rm /root/installer.php

ADD 99-settings.ini /etc/php.d/99-settings.ini

CMD ["/bin/sh"]

Instructions included in the above Docker file do as follows:

The above changes should add no more than 30 MB of new data. As I’ve already described, it’s using a copy-on-write strategy, and we could assume it should be a little more as we also modify some files.

Let’s try to build that and add some name and tag on that image for reference. As you see, every instruction indeed is using an intermediate container, and the new image is around 65% bigger than the base image - it’s over 150 MB.

$ docker build -t php:v1 ./php1/
Sending build context to Docker daemon  3.072kB
Step 1/12 : FROM centos:centos8
 ---> 589dc4d40385
Step 2/12 : RUN dnf -q -y module reset php
 ---> Running in 104930ffda97
Removing intermediate container 104930ffda97
 ---> 0d73e88e27c6
Step 3/12 : RUN dnf -q -y module enable php:7.3
 ---> Running in bdaadeafec8b
Removing intermediate container bdaadeafec8b
 ---> cda680d917bb
Step 4/12 : RUN dnf -q -y install epel-release
 ---> Running in 8be0d88b0be0
Removing intermediate container 8be0d88b0be0
 ---> 99ab8a737f2e
Step 5/12 : RUN dnf -q -y upgrade
 ---> Running in 7d972564aa8e
Removing intermediate container 7d972564aa8e
 ---> fedf09c194f0
Step 6/12 : RUN dnf -q -y install php-cli php-json php-mbstring php-mysqlnd php-pdo php-xml
 ---> Running in 1cd09a3a1438
Removing intermediate container 1cd09a3a1438
 ---> 8325c5f64e8d
Step 7/12 : RUN dnf -q -y clean all
 ---> Running in 6f3385a7e342
Removing intermediate container 6f3385a7e342
 ---> cf028cf94682
Step 8/12 : RUN curl -sL -o /root/installer.php https://getcomposer.org/installer
 ---> Running in c207d97cefff
Removing intermediate container c207d97cefff
 ---> c5938bd7ad65
Step 9/12 : RUN php /root/installer.php --install-dir=/usr/local/bin --filename=composer
 ---> Running in c301c4e7ed34
All settings correct for using Composer
Downloading...

Composer (version 1.9.2) successfully installed to: /usr/local/bin/composer
Use it: php /usr/local/bin/composer

Removing intermediate container c301c4e7ed34
 ---> e3f01f80687f
Step 10/12 : RUN rm /root/installer.php
 ---> Running in 04c845fb1d6d
Removing intermediate container 04c845fb1d6d
 ---> 71650e556073
Step 11/12 : ADD 99-settings.ini /etc/php.d/99-settings.ini
 ---> 5aea14562d5c
Step 12/12 : CMD ["/bin/sh"]
 ---> Running in d10d11faab7d
Removing intermediate container d10d11faab7d
 ---> 1e1a4b8c4fa5
Successfully built 1e1a4b8c4fa5
Successfully tagged php:v1

$ docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
php                 v1                  1e1a4b8c4fa5        2 minutes ago       391MB
centos              centos8             589dc4d40385        20 hours ago        237MB
alpine              latest              cc0abc535e36        3 weeks ago         5.59MB
centos              centos7             5e35e350aded        2 months ago        203MB

Let’s also run docker history command to check individual layers.

$ docker history php:v1
IMAGE               CREATED             CREATED BY                                      SIZE                COMMENT
1e1a4b8c4fa5        2 minutes ago       /bin/sh -c #(nop)  CMD ["/bin/sh"]              0B
5aea14562d5c        2 minutes ago       /bin/sh -c #(nop) ADD file:7a7b20378036a8b0c…   156B
71650e556073        2 minutes ago       /bin/sh -c rm /root/installer.php               0B
e3f01f80687f        2 minutes ago       /bin/sh -c php /root/installer.php --install…   1.94MB
c5938bd7ad65        2 minutes ago       /bin/sh -c curl -sL -o /root/installer.php h…   274kB
cf028cf94682        3 minutes ago       /bin/sh -c dnf -q -y clean all                  1.72MB
8325c5f64e8d        3 minutes ago       /bin/sh -c dnf -q -y install php-cli php-jso…   52.2MB
fedf09c194f0        3 minutes ago       /bin/sh -c dnf -q -y upgrade                    39.8MB
99ab8a737f2e        3 minutes ago       /bin/sh -c dnf -q -y install epel-release       23.1MB
cda680d917bb        3 minutes ago       /bin/sh -c dnf -q -y module enable php:7.3      12.1MB
0d73e88e27c6        3 minutes ago       /bin/sh -c dnf -q -y module reset php           22.2MB
589dc4d40385        20 hours ago        /bin/sh -c #(nop)  CMD ["/bin/bash"]            0B
<missing>           20 hours ago        /bin/sh -c #(nop)  LABEL =org.label-schema.s…   0B
<missing>           20 hours ago        /bin/sh -c #(nop) ADD file:aa54047c80ba30064…   237MB

When we look at layers with instructions that should remove files, we notice they either increase size even more or not change it at all. It may not be obvious, but it’s an outcome of a copy-on-write strategy. Those are our layers and instructions where we should seek for improvements.

How to decrease the image size?

The easiest and most common practice to decrease Docker image size is to reduce the number of instructions in Dockerfile. Almost all RUN instructions could be rewritten to one instruction. Let’s take a look at an improved version of our original Dockerfile.

FROM centos:centos8

RUN dnf -q -y module reset php; \
    dnf -q -y module enable php:7.3; \
    dnf -q -y install epel-release; \
    dnf -q -y upgrade; \
    dnf -q -y install php-cli php-json php-mbstring php-mysqlnd php-pdo php-xml; \
    dnf -q -y clean all; \
    curl -sL https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer

ADD 99-settings.ini /etc/php.d/99-settings.ini

CMD ["/bin/sh"]

All changes with dnf commands are self-explanatory, we run them as a one shell command. Similar to composer installation. Instead of downloading an installer to a file, run it, and then remove it, we redirect it to standard output and run PHP interpreter taking instructions from standard input.

Let’s build it and check if those changes decrease image size.

$ docker build -t php:v2 ./php2/
Sending build context to Docker daemon  3.072kB
Step 1/4 : FROM centos:centos8
 ---> 589dc4d40385
Step 2/4 : RUN dnf -q -y module reset php;     dnf -q -y module enable php:7.3;     dnf -q -y install epel-release;     dnf -q -y upgrade;     dnf -q -y install php-cli php-json php-mbstring php-mysqlnd php-pdo php-xml;     dnf -q -y clean all;     curl -sL https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
 ---> Running in 7f51150d1273
All settings correct for using Composer
Downloading...

Composer (version 1.9.2) successfully installed to: /usr/local/bin/composer
Use it: php /usr/local/bin/composer

Removing intermediate container 7f51150d1273
 ---> bb727791633c
Step 3/4 : ADD 99-settings.ini /etc/php.d/99-settings.ini
 ---> 450c17ab267f
Step 4/4 : CMD ["/bin/sh"]
 ---> Running in db4e53d5ef1b
Removing intermediate container db4e53d5ef1b
 ---> eadf78260b66
Successfully built eadf78260b66
Successfully tagged php:v2

$ docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
php                 v2                  eadf78260b66        30 seconds ago      276MB
php                 v1                  1e1a4b8c4fa5        6 minutes ago       391MB
centos              centos8             589dc4d40385        20 hours ago        237MB
alpine              latest              cc0abc535e36        3 weeks ago         5.59MB
centos              centos7             5e35e350aded        2 months ago        203MB

We managed to decrease the number of steps/layers and the size of the image. It’s 39 MB, which is about 17% bigger than the base one. Let’s once again run docker history to confirm the layers and sizes of our new image.

$ docker history php:v2
IMAGE               CREATED              CREATED BY                                      SIZE                COMMENT
eadf78260b66        About a minute ago   /bin/sh -c #(nop)  CMD ["/bin/sh"]              0B
450c17ab267f        About a minute ago   /bin/sh -c #(nop) ADD file:7a7b20378036a8b0c…   156B
bb727791633c        About a minute ago   /bin/sh -c dnf -q -y module reset php;     d…   38.5MB
589dc4d40385        20 hours ago         /bin/sh -c #(nop)  CMD ["/bin/bash"]            0B
<missing>           20 hours ago         /bin/sh -c #(nop)  LABEL =org.label-schema.s…   0B
<missing>           20 hours ago         /bin/sh -c #(nop) ADD file:aa54047c80ba30064…   237MB

Limitations, unwanted behavior, and possible problems

We need to be aware of some limitations when we shrink our Docker images.

Not all RUN commands could be combined into only one. Some of them may require external files which cannot be added before our only RUN command. On the other side, adding them after all commands are executed makes no sense. Think about configuration files that should overwrite the distribution ones.

When you’re creating multiple images with similar sets of commands, then the copy-on-write strategy starts to be very handy here. If commands are split into smaller pieces, the intermediate steps could be reused amongst multiple images, which decreases their overall size. It may be more beneficial than grouping commands. Try different setups in search of the best ratio.

In our examples, we run all commands, including dnf upgrade in one RUN command. It may introduce some problems. Whenever an upgrade would be necessary, the whole layer would be invalidated. Depending on your caching setup or images inheritance, such a layer may not be invalidated even when it should.

devopsdockerlinuxcentos
This site uses cookies to analyze traffic and for ads measurement purposes according to your browser settings. Access to those cookies is shared with Google to generate usage statistics and detect and address abuse. Learn more about how we use cookies.