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:
- My new image is based on an official CentOS 8 image.
- The default app stream module for PHP is being replaced with version 7.3.
- I’m adding an EPEL repository and upgrade the whole system.
- The next steps are installing some PHP packages and clean repository data to free some space.
- The next three instructions install Composer.
- In the end, a small config file for PHP is added to the image.
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.