Post

Tiny C++ container images with `-static`

Both gcc and clang support the -static compilation option. When we pass this option, we are asking the compiler to create a static executable. That is, an executable with no dynamic dependencies.

If we compile a “Hello World” program both with (with-static) and without (no-static) this option enabled, we can see the difference highlighted by ldd. For reference, I am using clang++-19 with the mold linker on an amd64 based Ubuntu 24.04 system.

ldd no-static 
	linux-vdso.so.1 (0x00007ffff7069000)
	libc++.so.1 => /lib/x86_64-linux-gnu/libc++.so.1 (0x00007311d370d000)
	libc++abi.so.1 => /lib/x86_64-linux-gnu/libc++abi.so.1 (0x00007311d36d1000)
	libunwind.so.1 => /lib/x86_64-linux-gnu/libunwind.so.1 (0x00007311d36c3000)
	libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007311d35da000)
	libgcc_s.so.1 => /lib/x86_64-linux-gnu/libgcc_s.so.1 (0x00007311d35ac000)
	libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007311d3200000)
	/lib64/ld-linux-x86-64.so.2 (0x00007311d382c000)

ldd with-static 
	not a dynamic executable

The use of the -static flag also has a massive impact on binary size due to the total lack of reuse of system library code (or, more specifically, any code!). Here are the sizes I got for my simple executable:

ExecutableSize
no-static22 KiB
with-static2071 KiB
with-static + -Wl,--gc-sections -Wl,--icf=safe -Wl,--strip-all1240 KiB

Even with some aggressive linker options, the -static binary is quite a lot larger!

This size cost may look scary, but one benefit we can extract from static binaries is when we deploy C++ applications in containers e.g. with Docker. Seeing as we won’t need to install any system libraries, we can just copy a binary into the smallest base image we can find and get a very small container image for that binary. The extra size of our binary could be tiny in comparison to needing an entire suite of potentially unused system libraries!

Smaller containers tend to have increased utilization benefits, such as:

  • Fast startup time.
  • Use less host system resources.
  • Run more containers on the same hardware.
  • Cheaper to run infrastructure.

These benefits can help with scaling up, saving money, and recovering quickly from failures.

The fact that we have built a static executable allows us to use use a Busybox base image. The musl c version of the BusyBox image is absolutely tiny, weighing in at a lightweight 1.45MB at the time of writing. Even if we add a few MBs with our chunky static executable, we are still going to have a very small container image!

Building such an image is straightforward because we need only copy the static executable somewhere visible and make it our entrypoint. Seeing as this post contains no actual code, I am going to assume this is a multi-stage Docker build and that we generated some build output in a prior builder stage.

1
2
3
FROM busybox:musl
COPY --from=builder /src/build/path/to/static-exe /usr/bin/static-exe
ENTRYPOINT [ "static-exe" ]

Pretty simple!

Now of course, the choice of static/dynamic linking and deployment platform are project dependent, but I think it’s pretty cool that it is possible to get such small images for our C++ applications in the right circumstances.

This post is licensed under CC BY 4.0 by the author.