Thanks to Python, I have gotten used to Docker images that takes minutes (or dozens of minutes) to build and hundreds of megabytes to store and upload.
FROM python3:3.14 # install everything RUN apt-get update \ && apt-get install -y --no-install-recommends libsome-random-library0 libanother-library0 \ libmaybe-its-for-pandas-i-dunno0 libit-goes-on0 liband-on0 \ liband-on-and-on0 \ && rm -rf /var/lib/apt/lists/* # install more of everything COPY requirements.txt . RUN pip3 install -r requirements.txt ENTRYPOINT ["gunicorn", "--daemon", "--workers=4", "--bind=0.0.0.0:8080", "app:create_app()"]
But there is another way. The Rust way. Where you build your image in a second, and it only takes 5 MB1.
COPY target/release/my-binary .
This is possible thanks to the points below.
- Rust is compiled to binary
- Rust is statically2 compiled to binary
- You can easily use musl3 with Rust
The first point means that there is no need for a runtime to interpret the script or the bytecode.
The second point means that the binary contains the code for all the libraries4. And, more specifically, only the required code. So no need to install them externally, you remove some overhead, and the total size is reduced.
With these two, you get down to just a few runtime dependencies. Namely, glibc and the ELF loader5.
$ ldd target/release/my-binary linux-vdso.so.1 (0x00007fffbf7f9000) libgcc_s.so.1 => /lib/x86_64-linux-gnu/libgcc_s.so.1 (0x00007f7a759b2000) libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007f7a758d3000) libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f7a756f2000) /lib64/ld-linux-x86-64.so.2 (0x00007f7a762a6000)
But you can go further! We do not have to use glibc. It turns out you can just statically compile musl. And with Rust, using musl is only a matter of installing
musl-tools and adding
--target=x86_64-unknown-linux-musl to your
cargo build command.
And, since we just got rid of our last runtime dependency, we do not even need the ELF loader! So now, we get:
$ ldd target/x86_64-unknown-linux-musl/release/my-binary statically linked
All the user code is now in a single file and will talk to the kernel directly using int 0x80.
This is a nice trick, and can help you if you really care about small Docker images, but it does have a drawback: targeting musl typically gives you lower performance. So keep this in mind!
- Sure, I am simplifying things here. In practice, you would also build the binary in a Dockerfile, and then use staging. But that does not change the point. ↩︎
- Static linking means including external libraries within the binary, as in opposition to dynamic libraries, where they are not included. Binaries that were dynamically linked will need to be provided with the external libraries at runtime (.so files on Linux, .dll files on Windows). ↩︎
- musl is an implementation of libc, the standard library for C. Virtually all binaries depend on the libc. glibc is the most common implementation of libc on Linux. ↩︎
- Of course, it won’t work if a library is using FFI to talk to OpenSSL for instance ↩︎
- The ELF loader is another program that helps your program to start up. In particular, it tells your program where to find the dynamically linked libraries. ↩︎