../

How to Create Ultra-Small, Ultra-Lightweight Docker Images

2025-05-16

Docker images built using Ubuntu as a base image often start at hundreds of megabytes, or even over a gigabyte. However, in reality, a running binary program doesn’t depend on many files. If we only keep the files absolutely necessary to run the program, the image size will undoubtedly be greatly reduced.

Here, we’ll take a Rust application as an example and try to package an image that can run ripgrep. Compiled languages like C/C++, Go, and others can follow the same method.

Preparing Files

Use the ldd command to view ripgrep’s dynamic link library dependencies:

ldd /usr/bin/rg

Result:

linux-vdso.so.1 (0x00007bd80ef30000)
libpcre2-8.so.0 => /usr/lib/libpcre2-8.so.0 (0x00007bd80e912000)
libgcc_s.so.1 => /usr/lib/libgcc_s.so.1 (0x00007bd80eefd000)
libc.so.6 => /usr/lib/libc.so.6 (0x00007bd80e722000)
/lib64/ld-linux-x86-64.so.2 => /usr/lib64/ld-linux-x86-64.so.2
(0x00007bd80ef32000)

The first entry is inserted into memory by the kernel and can be ignored. Therefore, rg requires a minimum of four .so files to run.

Create a new directory:

mkdir rootfs

Write a script to copy all these .so files to their corresponding locations in rootfs:

#!/bin/bash

copy_file () {
  local dir=$(dirname $1)
  mkdir -p rootfs/$dir
  cp $1 rootfs/$dir
}

for f in $(ldd /usr/bin/rg | grep '=>' | awk '{print $3}'); do
    copy_file $f
done

Run the script. Then note:

/lib64/ld-linux-x86-64.so.2 => /usr/lib64/ld-linux-x86-64.so.2
(0x00007bd80ef32000)

Copy ld-linux-x86-64.so.2 to rootfs/lib64.

mkdir -p rootfs/lib64
cp rootfs/usr/lib64/ld-linux-x86-64.so.2 rootfs/lib64/

Then copy the rg executable itself to rootfs:

cp /usr/bin/rg rootfs/

At this point, the file system for the Docker image is ready.

Creating the Image

Create the Dockerfile:

FROM scratch
COPY rootfs /
ENTRYPOINT ["/rg"]

Create the Docker image:

sudo docker build -t myrg .

Attempt to run it:

sudo docker run --rm myrg --version

Result: $ sudo docker run –rm myrg –version ripgrep 14.1.1

features:+pcre2
simd(compile):+SSE2,-SSSE3,-AVX2
simd(runtime):+SSE2,+SSSE3,+AVX2

PCRE2 10.43 is available (JIT is available)

The Docker image build is complete.

Check the size:

sudo docker images

Result:

REPOSITORY         TAG         IMAGE ID      CREATED      SIZE
localhost/myrg     latest      89ce8f41feaa  4 minutes ago  9.23 MB

As you can see, it is only 9.23 MB.

Chroot Jail

In fact, the principle behind this method is the same as creating a chroot jail; you can also run it using chroot:

sudo chroot rootfs /rg --version

In a real-world scenario, if you were actually deploying an application this way, it would be best to run it with a non-root user for security reasons.

sudo chroot --userspec 1000:1000 rootfs /rg --version

Limitations

Some applications, especially those in dynamic languages like Python, heavily rely on using dlopen to dynamically load .so files. These files cannot be found using ldd and thus cannot be packaged using this method. Although one could also use strace to analyze system calls and see exactly which files the program opened, it is still difficult to ensure that all dynamically linked libraries are copied and loaded.

Therefore, this method is generally only suitable for compiled languages, such as C/C++, Go, Rust, Haskell, and so on.


Mistivia - https://mistivia.com