Creating thin Docker containers for Rust binaries

Sometimes you need to distribute some tools as Docker containers, for example when you are running some long-running processes in a Kubernetes cluster. Or whatever else, I don't judge. Since nowadays I write all CLI tooling in Rust because it's just pure joy, I decided I need to make myself a small Dockerfile template for building thin images that won't take much disk space and are fast to download and run with only the bare minimum.

To ensure we're gonna end up with a really small Docker image, we need to make it a multi-stage build one. My original plan seemed easy enough. Compile a static binary using the Rust Linux musl toolchain in the first stage. Then move only the binary to a scratch image in the second stage. The scratch image is the most minimal Docker image you can use, with no extra layers, and nothing else in it really.

There are official Rust Docker images, so that's what I chose as a build stage base. They include rustup, so I just installed the musl toolchain and musl-tools, and I thought I was ready to go. However, when I tried to build the image, the build step in the first phase panicked with the following error:

thread 'main' panicked at '

Could not find directory of OpenSSL installation, and this `-sys` crate cannot
proceed without this knowledge. If OpenSSL is installed and this crate had
trouble finding it,  you can set the `OPENSSL_DIR` environment variable for the
compilation process.

Make sure you also have the development packages of openssl installed.
For example, `libssl-dev` on Ubuntu or `openssl-devel` on Fedora.

Of course, I thought, I'm doing some networking, nowadays encrypted connections are a must. So let's just install libssl-dev, as the error suggests. Close, but no cigar. Musl and openssl-sys-extra didn't play well since the SSL package in Ubuntu is linked against glibc. Makes sense. What's next? I didn't really feel like compiling OpenSSL myself.

With a lit bit of searching, I found the great clux/muslrust Docker image. It's based on Ubuntu Xenial and conveniently has built a lot of things with musl-gcc, like OpenSSL. A big thanks for this effort to clux, the creator!

After replacing the rust image with clux/muslrust the crate compilation went fine and I thought that's that. But since I kinda rushed it, I totally forgot the small tiny detail of CA certificates. The Docker image was fine, however, the binary was basically not able to do any HTTPS requests since they would error out with certificate errors. Remember, the scratch image is containing basically just the binary.

So after a couple of extra steps to install the CA certificates Ubuntu package and then copy over the certificates from the first to the second stage, I was done. And here's the final result.

FROM clux/muslrust AS build
WORKDIR /usr/src

# Update CA Certificates
RUN apt update -y && apt install -y ca-certificates
RUN update-ca-certificates

# Build dependencies and rely on cache if Cargo.toml
# or Cargo.lock haven't changed
RUN USER=root cargo new <YOUR_CRATE>
WORKDIR /usr/src/<YOUR_CRATE>
COPY Cargo.toml Cargo.lock ./
RUN cargo build --target x86_64-unknown-linux-musl --release

# Copy the source and build the application.
COPY src ./src
RUN cargo install --target x86_64-unknown-linux-musl --path .

# Second stage
FROM scratch

# Copy the CA certificates
COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
# Copy the statically-linked binary to the second stage
COPY --from=build /root/.cargo/bin/<YOUR_CRATE> .
USER 1000

CMD ["./<YOUR_CRATE>"]

This basic Dockerfile template is available as a GitHub Gist. I use it in a couple of places, one of which is a public crate called rmq_monitor. The image size turns out to be a tad more than your binary, in the case of the rmq_monitor image it's only 7Mb uncompressed.