Baeldung Pro – Ops – NPI EA (cat = Baeldung on Ops)
announcement - icon

Learn through the super-clean Baeldung Pro experience:

>> Membership and Baeldung Pro.

No ads, dark-mode and 6 months free of IntelliJ Idea Ultimate to start with.

1. Overview

A Docker image is a blueprint of the applications, libraries, and software installed that can be instantiated as a container. We “flatten” an image to reduce its size, number of constituent layers, and storage space, making it more lightweight.

In this tutorial, we explore the best practices and methods for flattening a Docker image. We’ll discuss five methods:

  • Using a lightweight base image when creating a new image
  • Using docker export and docker import when we need to reduce the image size and number of image layers, using an existing container
  • Using a multi-stage Docker image build to exclude unneeded files, libraries, and binaries used in the build process from the final build
  • Using the docker-squash tool when we need to reduce the size and number of layers of an existing image
  • Using docker save when we want to reduce the storage space for an image

To follow along, we’ll only need a Linux machine with Docker installed.

2. Using a Lightweight Base Image

Ideally, we should choose a lightweight Docker image as the base image for a new image.

For example, we should choose the minimal ubuntu image for the Ubuntu OS when we only use the UTF-8 locale. We do this by specifying it in the FROM command in a Dockerfile:

FROM ubuntu

Similarly, we can choose the minimal alpine image for Alpine Linux.

3. Using docker export and docker import

We can use the docker export command to export a container’s filesystem as a tar archive, saving a Docker container’s state. Subsequently, we can use the docker import command to import the tar archive to create a new image.

Typically, this method reduces an image’s size because the docker export command doesn’t export the contents of volumes associated with the container. Also, it reduces the number of image layers to one.

To demonstrate with an example, let’s pull the mysql image:

$ docker pull mysql
Using default tag: latest
latest: Pulling from library/mysql
c2eb5d06bfea: Pull complete
ba361f0ba5e7: Pull complete
0e83af98b000: Pull complete
770e931107be: Pull complete
a2be1b721112: Pull complete
68c594672ed3: Pull complete
cfd201189145: Pull complete
e9f009c5b388: Pull complete
61a291920391: Pull complete
c8604ede059a: Pull complete
Digest: sha256:2247f6d47a59e5fa30a27ddc2e183a3e6b05bc045e3d12f8d429532647f61358
Status: Downloaded newer image for mysql:latest
docker.io/library/mysql:latest

Notice the numerous image layers, each represented by a line that ends with “Pull Complete”.

Next, let’s list the image, both to verify that it gets downloaded and to find the image’s size:

$ docker images
REPOSITORY   TAG       IMAGE ID       CREATED       SIZE
mysql        latest    2c849dee4ca9   4 weeks ago   859MB

We’ll need to run a container so that we can export the container’s filesystem to a tar archive:

$ docker run --name mysql-db -e MYSQL_ROOT_PASSWORD=mysql -d mysql

Next, let’s export the mysql-db container with the docker export command:

$ docker export -o mysql-db.tar mysql-db

And, we can verify the tar archive gets created by listing all files:

$ ls -l
-rw-------   1 ubuntu ubuntu 848896512 May 16 22:31 mysql-db.tar

We’re ready to import the tar archive to a new image – let’s call it mysql-db:imported:

$ docker import mysql-db.tar mysql-db:imported

Again, let’s list the Docker images:

$ docker images
REPOSITORY   TAG         IMAGE ID       CREATED          SIZE
mysql-db     imported    af0f5e80592f   8 seconds ago    830MB
mysql        latest      2c849dee4ca9   4 weeks ago      859MB

Voilà, the image size is reduced by 29 MB. Furthermore, we can pipe the output from the docker export command to the docker import command to avoid generating the intermediate tar archive:

$ docker export mysql-db | docker import - mysql:flattened

Again, the image size is reduced by 29MB:

$ docker images
REPOSITORY   TAG         IMAGE ID       CREATED          SIZE
mysql        flattened   6199c9a961a9   10 seconds ago   830MB
mysql        latest      2c849dee4ca9   4 weeks ago      859MB

Additionally, with either method, the number of image layers is reduced to one:

$ docker image inspect mysql:flattened -f '{{.RootFS.Layers}}'

4. Using a Multi-Stage Build

With multi-stage builds, we use multiple FROM commands in the same Dockerfile. Each FROM instruction begins a new stage of the build.

We can benefit by selectively copying only the required artifacts from one stage to another. Further, we can copy variables between stages. As a result, the final image is smaller than if we were to include all intermediate artifacts.

Let’s demonstrate using a Hello World Go application hello.go:

package main

import "fmt"

func main() {
    fmt.Printf("Hello, world.\n")
}

And let’s dockerize it with a single-stage Dockerfile:

ARG GOLANG_VER=1.8
FROM golang:${GOLANG_VER} as builder
WORKDIR /go/src/app
COPY . .
RUN go-wrapper download 
RUN go-wrapper install
CMD ["/go/bin/app"]

If we build a Docker image called single-stage-hello:

$ docker build -t single-stage-hello .

Then we can verify that the new image is built:

$ docker images
REPOSITORY           TAG         IMAGE ID       CREATED              SIZE
single-stage-hello   latest      bcb3d5ef8d45   About a minute ago   2.92GB

Let’s verify that a new container prints the message encoded in the hello.go file:

$ docker run --rm single-stage-hello
Hello, world.

For a compiled language like Go, we use a multi-stage build to compile in the first stage. In the second stage, we simply copy the compiled binaries into a final runtime image. We do this to avoid bundling the entire compiler in the final image.

To demonstrate, let’s use a multi-stage Dockerfile:

ARG GOLANG_VER=1.8
FROM golang:${GOLANG_VER} as builder
WORKDIR /go/src/app
COPY . .
RUN go-wrapper download 
RUN go-wrapper install

FROM scratch
COPY --from=builder /go/bin/app /app
CMD ["/app"]

Again, let’s build a Docker image, called hello this time:

$ docker build -t hello .

And now let’s get the image sizes:

$ docker images
REPOSITORY           TAG         IMAGE ID       CREATED              SIZE
single-stage-hello   latest      bcb3d5ef8d45   20 minutes ago       2.92GB
hello                latest      df4329120a36   About a minute ago   1.56MB

Notice the reduction in the image size achieved by using a multi-stage build – from 2.92 MB to 1.56 MB. Again, let’s run a new container:

$ docker run --rm hello
Hello, world.

Indeed, it still prints our “Hello, world.” message.

5. Using the docker-squash Tool

We can use the docker-squash Python library to squash an existing Docker image to remove unnecessary temporary files. To demonstrate with an example, we need to install Python3, and Pip:

$ sudo apt install python3
$ sudo apt install python3-pip

We also need to create a separate virtual environment, let’s say docker-squash-env, and activate it:

$ python3 -m venv docker-squash-env
$ source docker-squash-env/bin/activate

Then, we can install the docker-squash library:

(docker-squash-env) ubuntu@ip-address:~$ pip install docker-squash

We’re ready to squash an image; let’s use the same example image from earlier, mysql, and squash it to a new image called mysql:squashed:

$ docker-squash -t mysql:squashed mysql

And then let’s list the images to verify that a new squashed image was created:

$ docker images
REPOSITORY   TAG         IMAGE ID       CREATED              SIZE
mysql        squashed    9cffd364951a   About a minute ago   830MB
mysql        latest      2c849dee4ca9   4 weeks ago          859MB

Notice the 29 MB reduction in size in the new image mysql:squashed. It’s no coincidence that the reduction in size is the same as when we used docker export and import commands – docker-squash uses those two commands internally.

6. Using docker save

Similar to using docker export and import, we can use the docker save and load. Though note that our goal this time is to optimize storage space when archiving a set of images instead of a container. To save even more space, we can save an image to a tar.gz file using gzip.

To demonstrate with an example, let’s again use the mysql image:

$ docker save mysql:latest | gzip > mysql.tar.gz

After the command completes, verify by listing files that the tar.gz is much smaller than the mysql image:

$ ls -l
-rw-rw-r--   1 ubuntu ubuntu 254347383 May 16 23:07 mysql.tar.gz

Thereafter, we can delete the mysql image:

$ docker image rm mysql

Accordingly, when we need to, we can restore the mysql image from the tar.gz file using the docker load command:

$ docker load < mysql.tar.gz
Loaded image: mysql:latest

7. Conclusion

In this article, we learned about the different methods to “flatten” a Docker image using various examples. We should use a suitable method depending on whether we need to choose a smaller base image, build a smaller image, export an existing container to a smaller image, squash an existing image, or simply reduce the storage space associated with an image.

As always, the Docker commands used in this article are available over on GitHub.