How to Package Rust Applications Into Minimal Docker Containers

Learning Rust has been on my personal backlog for a while now. As I started digging in, I decided to build a small web application as an exercise. Once I had an initial version of the app working, I wanted to package it up as a docker image so that I could later deploy it to a Kubernetes cluster.

In the Go world, it is common to use docker’s multi-stage build feature to produce the app’s docker image in two stages. The first stage uses the golang image and is where we build the application into a statically-linked binary. Once built, we copy the binary into a scratch container in the second stage. The result is a rather small image that has nothing other than the application.

I was looking to do the same thing for my Rust application. While I was at it, I also wanted to leverage the docker build cache to avoid having to download crates on every docker build.

After reading a couple of posts online, this is the Dockerfile that I ended up writing for my app:

# Dockerfile for creating a statically-linked Rust application using docker's
# multi-stage build feature. This also leverages the docker build cache to avoid
# re-downloading dependencies if they have not changed.
FROM rust:1.35.0 AS build
WORKDIR /usr/src

# Download the target for static linking.
RUN rustup target add x86_64-unknown-linux-musl

# Create a dummy project and build the app's dependencies.
# If the Cargo.toml or Cargo.lock files have not changed,
# we can use the docker build cache and skip these (typically slow) steps.
RUN USER=root cargo new url-shortener
WORKDIR /usr/src/url-shortener
COPY Cargo.toml Cargo.lock ./
RUN cargo build --release

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

# Copy the statically-linked binary into a scratch container.
FROM scratch
COPY --from=build /usr/local/cargo/bin/url-shortener .
USER 1000
CMD ["./url-shortener"]

When it comes to building docker images, one of the best practices is to keep your images small. This reduces the number of “things” in your container, and thus reduces its attack surface. Furthermore, it reduces download times and disk usage.

With the Dockerfile above, the resulting docker image is 4.6 MB in size, compared to a whopping 2.15 GB if we were to use the rust image as the base layer. That is a big difference.

Did you find this post useful? Did I get something wrong? I would love to hear from you! Please reach out via @alexbrand.