Creating a tiny Docker image of a Rust project
April 22, 2020 [Docker, Programming, Rust, Tech]I am building a toy project in Rust to help me learn how to deploy things in AWS. I'm considering using Elastic Beanstalk (AWS's platform-as-a-service) and also Kubernetes. Both of these support deploying via Docker containers, so I am learning how to package a Rust executable as a Docker image.
My program is a small web site that uses Redis as a back end database. It consists of some Rust code and a couple of static files.
Because Rust has good support for building executables with very few dependencies, we can actually build a Docker image with almost nothing in it, except my program and the static files.
Thanks to Alexander Brand's blog post How to Package Rust Applications Into Minimal Docker Containers I was able to build a Docker image that:
- Is very small
- Does not take too long to build
The main concern for making the build faster is that we don't download and build all the dependencies every time. To achieve that we make sure there is a layer in the Docker build process that includes all the dependencies being built, and is not re-built when we only change our source code.
Here is the Dockerfile I ended up with:
# 1: Build the exe FROM rust:1.42 as builder WORKDIR /usr/src # 1a: Prepare for static linking RUN apt-get update && \ apt-get dist-upgrade -y && \ apt-get install -y musl-tools && \ rustup target add x86_64-unknown-linux-musl # 1b: Download and compile Rust dependencies (and store as a separate Docker layer) RUN USER=root cargo new myprogram WORKDIR /usr/src/myprogram COPY Cargo.toml Cargo.lock ./ RUN cargo install --target x86_64-unknown-linux-musl --path . # 1c: Build the exe using the actual source code COPY src ./src RUN cargo install --target x86_64-unknown-linux-musl --path . # 2: Copy the exe and extra files ("static") to an empty Docker image FROM scratch COPY --from=builder /usr/local/cargo/bin/myprogram . COPY static . USER 1000 CMD ["./myprogram"]
The FROM rust:1.42 as build line uses the newish Docker feature multi-stage builds - we create one Docker image ("builder") just to build the code, and then copy the resulting executable into the final Docker image.
In order to allow us to build a stand-alone executable that does not depend on the standard libraries in the operating system, we use the "musl" target, which is designed to be statically linked.
The final Docker image produced is pretty much the same size as the release build of myprogram, and the build is fast, so long as I don't change the dependencies in Cargo.toml.
A couple more tips to make the build faster:
- Use a .dockerignore file. Here is mine:
/target/ /.git/
- Use Docker BuildKit, by running the build like this:
DOCKER_BUILDKIT=1 docker build .