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.
Last updated: June 2, 2025
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:
To follow along, we’ll only need a Linux machine with Docker installed.
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.
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}}'
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.
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.
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
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.