Container - Smaller Images

Building your own containers is very easy and fast to learn. But very soon, you may end up with large container images and no idea how to reduce the footprint. Since you need to download and store each image, it can be useful to minimize the images, and this article will address exactly this.

Container - Smaller Images

Building your own containers is very easy and fast to learn. But very soon, you may end up with large container images and no idea how to reduce the footprint. Since you need to download and store each image, it can be useful to minimize the images, and this article will address exactly this.

Building Images

Building your own images is very common for most developers and operators. You want to add packages to a standard image or provide base images for your team. You may also want to package your application to deliver it to customers or deploy it on your infrastructure.

This tutorial uses Podman to demonstrate the build process, but it can be adapted with Docker, Kaniko, Containerd or Buildah, by replacing the commands.

If you never build an image before, you may want to read about it in the "Podman - Images" article, first. It provides an overview of the concept and process and explains what happens during the build process.

Smaller images

Reducing the footprint of container images can have a huge impact on your container workflow. Huge images will take some time to be updated, downloaded and started.

In addition, you need to provide proper disk space for your container infrastructure, which cannot be used for your data. This gets even worse in development environments, where you will download and start only slightly changed images over and over again.

But, this is also an issue in IoT environments, where you have limited disk space and bandwidth or in cloud environments, where you have to pay for every Gigabyte of data.

Smaller images also have a smaller attack surface. If you don't have something installed, it cannot be broken. Since you want to keep your images secure and patched, you will need to update more packages more often with larger images.

Demo application

For this tutorial, I want to have a very minimal Go-Application. It should respond with a simple "Hello, Community!". Building this application is quite easy. So, let's do it!

First, we will need a simple directory layout.

smaller-containers/
|- app/
|  |- hello.go
|
|- README.md
Directory Layout

Create the above file "hello.go" in the "app" directory with the below content.

package main
  
import "fmt"
  
func main() {
    fmt.Println("Hello, Community!")
}
app/hello.go

Now, we need to build this application and test, if it is working. This can be done in two ways.

If you have Go installed on your system, you can run the bellow commands.

# Create a go.mod file
$ go mod init hello

# Build the application
$ go build hello.go

# Run the application
$ ./hello
Hello, Community!

If you don't have Go installed, you can do the same, but with the help of containers.

# Create a go.mod file
$ podman run -it --rm -v $PWD:/usr/src/app:Z -w /usr/src/app docker.io/library/golang go mod init hello

# Build the application
$ podman run -it --rm -v $PWD:/usr/src/app:Z -w /usr/src/app docker.io/library/golang go build hello.go

# Run the application
$ ./hello
Hello, Community!

If this works fine, you can continue.

From LARGE to small

We will start with a pretty common example (building and running the demo application in a container), that you may use on your own. Each consecutive step will teach us something and reduce the image size.

Baseline

Let's start with something you find in many tutorials.

smaller-containers/
|- app/
|  |- hello.go
|
|- Containerfile
|- README.md
Directory Layout

Create the Containerfile with the below content in our directory layout.

FROM registry.fedoraproject.org/fedora:35

RUN dnf -y install golang

WORKDIR /usr/local/src/hello

COPY app/ /usr/local/src/hello

RUN go mod init hello
RUN go build -o /usr/local/bin/hello hello.go

CMD ["/usr/local/bin/hello"]
Containerfile

Now, let's build the container image.

# Build the container image
$ podman image build -t hello:latest .

# Run the resulting image
$ podman container run --rm hello:latest
Hello, Community!

And check the actual size.

# List images
$ podman image ls
REPOSITORY                         TAG         IMAGE ID      CREATED         SIZE
localhost/hello                    latest      0e2d43e2088f  25 seconds ago  1.09 GB

As you can see, 1009 MB is pretty huge.

Clean Up

Let's start with some cleanup. During an installation, Fedora will download metadata and caching data, which is still present in the image. Removing this immediately will reduce the resulting image size.

Adjusting the Containerfile as described below will do the trick.

FROM registry.fedoraproject.org/fedora:35

RUN dnf -y install golang && \
    dnf clean all

WORKDIR /usr/local/src/hello

COPY app/ /usr/local/src/hello

RUN go mod init hello
RUN go build -o /usr/local/bin/hello hello.go

CMD ["/usr/local/bin/hello"]
Containerfile

Now, build, run and list the image again.

# Build the container image
$ podman image build -t hello:latest .

# Run the resulting image
$ podman container run --rm hello:latest
Hello, Community!

# List images
$ podman image ls
REPOSITORY                         TAG         IMAGE ID      CREATED         SIZE
localhost/hello                    latest      95d41ec9c4ee  12 seconds ago  842 MB

842 MB is already a bit better, but still huge.

Unwanted packages

Limiting the installed packages will also help to reduce the overall size of the resulting image. DNF supports two interesting switches for our purpose.

The flag --nodocs will skip the installation of documentation packages, whereas --setopt install_weak_deps=False suppresses the installation of weak dependencies.

FROM registry.fedoraproject.org/fedora:35

RUN dnf -y install golang \
      --nodocs \
      --setopt install_weak_deps=False && \
    dnf clean all

WORKDIR /usr/local/src/hello

COPY app/ /usr/local/src/hello

RUN go mod init hello
RUN go build -o /usr/local/bin/hello hello.go

CMD ["/usr/local/bin/hello"]
Containerfile

The result will be even smaller again in most cases.

# Build the container image
$ podman image build -t hello:latest .

# Run the resulting image
$ podman container run --rm hello:latest
Hello, Community!

# List images
$ podman image ls
REPOSITORY                         TAG         IMAGE ID      CREATED         SIZE
localhost/hello                    latest      a4f9bdc19aed  33 seconds ago  737 MB

But still, 737 MB seems too large for me.

Multi-stage builds (1)

For this example, where we want to build an application and only ship a resulting image, you can also use Multistage builds. The build of our Go application will be done in one container, and only the binary will be shipped in the resulting container.

# Build Image
FROM registry.fedoraproject.org/fedora:35 AS gobuild

RUN dnf -y install golang \
      --nodocs \
      --setopt install_weak_deps=False && \
    dnf clean all

WORKDIR /usr/local/src/hello

COPY app/ /usr/local/src/hello

RUN go mod init hello
RUN go build -o /usr/local/bin/hello hello.go

# Final Image
FROM registry.fedoraproject.org/fedora:35

COPY --from=gobuild /usr/local/bin/hello /usr/local/bin/hello

CMD ["/usr/local/bin/hello"]
Containerfile

In the first line, we are naming the build image. Afterwards, we will install Golang again and build our application. The new part will copy the resulting binary from our build image to a new image and set the proper command.

Let's see how the result looks like.

# Build the container image
$ podman image build -t hello:latest .

# Run the resulting image
$ podman container run --rm hello:latest
Hello, Community!

# List images
$ podman image ls
REPOSITORY                         TAG         IMAGE ID      CREATED         SIZE
localhost/hello                    latest      3a52f42328f6  2 minutes ago   161 MB

Pretty awesome, so far. Only 161 MB seems like a nice achievement.

Multi-stage builds (2)

With Golang, we can even go a step further. Golang applications will link all needed dependencies and libraries statically. In fact, we don't even need a Fedora environment (or any other) to run our application.

Let's reduce the size even more.

> cat Containerfile 
# Build Image
FROM registry.fedoraproject.org/fedora:35 AS gobuild

RUN dnf -y install golang \
      --nodocs \
      --setopt install_weak_deps=False && \
    dnf clean all

WORKDIR /usr/local/src/hello

COPY app/ /usr/local/src/hello

RUN go mod init hello
RUN go build -o /usr/local/bin/hello hello.go

# Final Image
FROM scratch

COPY --from=gobuild /usr/local/bin/hello /usr/local/bin/hello

CMD ["/usr/local/bin/hello"]
Containerfile

Our resulting image will be build FROM scratch, but does this work? And how does the result look like?

# Build the container image
$ podman image build -t hello:latest .

# Run the resulting image
$ podman container run --rm hello:latest
Hello, Community!

# List images
$ podman image ls
REPOSITORY                         TAG         IMAGE ID      CREATED         SIZE
localhost/hello                    latest      6974dc3b38a5  7 seconds ago   1.94 MB

So, this is awesome. Only 1.94 MB as the result. Since we came from over 1 GB, I assume we achieved a lot.

In some future articles, I will demonstrate how you can do similar things with other software, like NodeJS or my lovely Apache httpd example. ;)

The web is full of cool ideas and examples about Image builds and optimizations. Below you can find some articles and documentation. One of them is actually written from myself.

Build smaller containers - Fedora Magazine
Bloated containers waste resources, slow things down, and can even cost you extra money! Here are a few tips on shrinking your containers!
Use multi-stage builds
Keeping your images small with multi-stage images
3 simple tricks for smaller Docker images
When it comes to building Docker containers, you should always strive for smaller images. Images that share layers and are smaller in size are quicker to transfer and deploy. But how do you keep the size under control when every RUN statement creates a new layer, and you need intermediate artefacts …
Building Smaller Container Images - Fedora Magazine
Learn of a few tips to build smaller container images using Fedora’s minimal base image.

Conclusion

Working with containers can be really fun. And the next time you are building a container, you can reduce the footprint and surprise your community, colleagues and friends.

Do you know other cool technics to reduce the footprint? I would love to hear from it and learn something new.