EnglishPolski
Let's compare docker image sizes
Docker Containers

Let's compare docker image sizes

While working on a separate project, I needed to create a simple echoserver. As I wanted to run it inside container, I decided to use pretty standard flow of multi-stage build. But then I thought: It would be good to write something about it.

What is a multi-stage build

It’s a technique for building container image that allows to drastically reduce the size of container image. That’s pretty much all the theory. But what does it mean in practice?

Standard container images comes with quite big amount of pakcages included, that will not be used during application uptime. They will just “lie and get dusty”, but in the same time, at least some of them may be required for building process.

So it owuld be the best, to have them during build time, but also remove them for the final image.

But this have a small downside.

Few bits of theory

To achieve that we need to make sure that our application will work properly without any dependencies. That is, it must be “linked statically”. To be precisely simple: we must elimnate all external dependencies. To avoid unnecessary increasement in size, most applications dynamically load “shared libraries”. These are packages that are present inside the operating system, or being installed with some other programs (like Windows Redistributables, which had to be installed to pretty much any game about 10-15 years ago)

Statically linked application have all dependencies packed inside the executable file.

That’s a bit counterintuitive: in order to reduce the image size of container at all, we need to increase the size of application.

But that’s enough of theory.

Let’s do some comparison.

I’ve mentioned creating simple echoserver application for other project that I’m woirking on. It’s created in Golang, and that’s exactly what I wanted to have - no external dependencies.

You can find it on my github.

Comparison

Let’s begin by building the echoserver, using Golang compiler, as it is. You will se direct output from my terminal

ls -lh echoserver
-rwxrwxrwx 1 andrzej andrzej  369 Feb 27 21:40 Dockerfile
-rwxrwxrwx 1 andrzej andrzej  565 Feb 27 21:40 README.md
-rwxrwxrwx 1 andrzej andrzej 7.8M Feb 27 21:40 echoserver
-rwxrwxrwx 1 andrzej andrzej   89 Feb 27 21:40 go.mod
-rwxrwxrwx 1 andrzej andrzej  163 Feb 27 21:40 go.sum
-rwxrwxrwx 1 andrzej andrzej 1.1K Feb 27 21:40 main.go
Command parameters

Ls command parameters (always worth to remind the basics)

  • -l - means long - outputs access rights, owner, group, size and modification date
  • -h - mean human - output file/dir sizes in more human-readable units, here megabytes instead of bytes

So, the compiled binary of echoserver is 7.8 megabytes in size. Source code for this file have 52 lines, and uses one binary cloned from github. I guess that using net/http from standard packages inside golang could reduce the size further, but this doesn’t really matter right now.

Container sizes

Inside dockerfile of this project - i’ve used alpine linux based image. As we can find on dockerhub:

A minimal Docker image based on Alpine Linux with a complete package index and only 5 MB in size!

I’ve used alpine 3.23.3, which was the newest, except from edge (I assume this is pretty much the same as latest) at the moment of creating this article.

According to my list of container images:

docker image ls | grep alpine
alpine:3.23.3                  a40c03cbb81c       8.44MB             0B

A bit more than I’ve expected, but if we compare this to Ubuntu, for example (ubuntu:26.04)

docker image ls | grep ubuntu
ubuntu:26.04                   ec9077217931       88.2MB             0B

88.2 MB - That’s 10x the size of alpine.

Surprise surprise
I must say that I was surprised as well when seeing this for the first time. Ubuntu is widely considered a heavy image. And according to my memory from about 5 years ago, the size of ubuntu image was over 250Mb (that’s my personal remeberance, may not be correct)

It’s pretty clear why anybody should prefer using alpine, or any else minimal distro as a base for container, right?

So let’s see the size of echoserver container after building it using provided Dockerfile:

docker image ls | grep echoserver
echoserver:1.0.0               e958e8eca88c       17.4MB             0B   U

Looks legit. 7.8 + 8.44MB = 16.2MB, plus some overhead for bash and othe stuff. Not bad at all.

Even more radical way

Let’s try to create even smaller image. We can do it by using “scratch” as a base image. “Scratch” is a special tag used for image that is just empty. No shell, no package manager, nothing. Just blank space for application.

Let’s start by modyfing the Dockerfile a bit:

ARG SERVER_PORT=8080
ARG BASE_IMAGE=golang:1.26.0
ARG ALPINE_IMAGE=scratch # replaced alpine with scratch

# build stage
FROM ${BASE_IMAGE} AS builder
WORKDIR /app
COPY . .
RUN go mod download
RUN CGO_ENABLED=0 GOOS=linux go build -o echoserver .

# minimal runtime with alpine
FROM ${ALPINE_IMAGE}
WORKDIR /app
COPY --from=builder /app/echoserver .
EXPOSE ${SERVER_PORT}
CMD ["./echoserver"]

I’ve called it “minimal” during my build:

docker build -t echoserver:minimal .

And the result is:

docker image ls | grep echoserver
echoserver:1.0.0               e958e8eca88c       17.4MB             0B   U
echoserver:minimal             9ec53ab7757f       8.95MB             0B

Looks like the size in this case about 50% less than previous one. That’s nice result, but we need to remember that this is not for free. Total minimalism like this comes with potental difficulties with debugging. There is no shell, no packages. Just plain application, and it’s logs. If everything is fine, then you will receive lower bill for you container repository.

But if something goes wrong, problems with debugging can turn into a nightmare. Copying execs to container, moving files around, if something crashes, you will need to start all over again. That’s not fun. But sometimes can be worth it.

Just don’t do it blindly.

Conclusion

Multi stage builds are brilliant. And so does minimalism in container image architecture, as size is not only factor that we should tak into consideration here. Less packages also mean less potential vulnerabilites, but so does more problems with debugging. This is a tradeoff. But can be worth it, especially if you know what are you doing.

Back to Top