Cache Dependencies in Rust Docker Builds

| tech

This is a hack for incremental Docker builds with Rust. If you follow a naive approach, that is, just copy your files and run cargo build, Rust will download and compile all your dependencies each time you build your image. The layer before the compilation step will change each time you modify your code and cargo build has no option to only build dependencies.

First create a dummy Cargo package with a library target, copy manifest and lock file, and then perform a release build of the library. I use the library target to avoid having to supply dummy files for (potentially multiple) binary targets specified in the manifest. As long as your dependencies don't change, the layer created by the first release build can be re-used.

After this first step, copy all your files and perform another release build. It's important to update the timestamp of your lib.rs (by touching the file), otherwise its last modification will predate the last build, so it won't be compiled and you'll run into errors.

RUN cargo init --lib --vcs none
COPY Cargo.toml Cargo.lock ./
RUN cargo build --release --lib

COPY . .
RUN touch src/lib.rs && cargo build --release

Here is a complete Dockerfile for a Rust project building a binary target bin-target for reference. The final image doesn't need the Rust toolchain because we simply copy the binary.

FROM rust:1.66-slim AS builder

WORKDIR /app

RUN cargo init --lib --vcs none
COPY Cargo.toml Cargo.lock ./
RUN cargo build --release --lib

COPY . .
RUN touch src/lib.rs && cargo build --release


FROM debian:bullseye-slim AS runtime

WORKDIR /app
COPY --from=builder /app/target/release/bin-target bin-target
ENTRYPOINT ["./bin-target"]

Not a hacker? If you don't mind pulling in another dependency, you can use cargo-chef instead, a tool by Luca Palmieri created exactly for this purpose.