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:
Executable | Size |
no-static | 22 KiB |
with-static | 2071 KiB |
with-static + -Wl,--gc-sections -Wl,--icf=safe -Wl,--strip-all | 1240 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.