Rust & Dockerfile : cache dependencies with cargo chef
Daniel Di Dio Balsamo
Posted on March 15, 2024
Introduction
Let's consider this basic Dockerfile:
# syntax=docker/dockerfile:1
# build in rust env
FROM rust:latest AS builder
WORKDIR /app
COPY . .
RUN cargo build --release
# run in a minimal image
FROM debian:bookworm-slim
COPY --from=builder /app/target/release/hello_world /usr/local/bin/hello_world
CMD hello_world
This Dockerfile produces a valid image, but think about an application with a lot of dependencies : even if you only update application code, all the dependencies are built again. This is time consuming.
Ideally, we should have something like cargo build --dependencies-only
, but such a dream hasn't come true yet.
There are many ways to work around this issue.
The least effective solution is to temporary create an empty main.rs
using RUN mkdir src && echo "fn main() {}" > src/main.rs
. This way, only dependencies are considered during the first build, then copy application code and build again.
It works since first you build with deps only (1 layer), then with your code (another layer) : if your own code changes, the deps layer doesn't change so it's not rebuilt.
In addition of not being efficient, this solution can be hard to maintain: what if Cargo.toml
contains a local dependency ? What about workspaces ?
You're going to use sed
with a lot of particular cases to handle, and every time Cargo.toml
changes, you're going to update your sed commands in Dockerfile
.
We can do much better with cargo chef
.
cargo-chef solution
This crate caches the dependencies for you. You don't have to install it locally, just use it during docker build.
Let's see how it works :
# syntax=docker/dockerfile:1
# (1) create rust env with cargo chef crate
FROM rust:latest AS chef
WORKDIR /app
RUN cargo install cargo-chef
# (2) generate recipe file to prepare dependencies build
FROM chef AS planner
COPY . /app
RUN cargo chef prepare --recipe-path recipe.json
# (3) build dependencies
FROM chef AS cacher
COPY --from=planner /app/recipe.json recipe.json
RUN cargo chef cook --release --recipe-path recipe.json
# (4) build app
FROM chef AS builder
COPY . /app
COPY --from=cacher /app/target target
COPY --from=cacher /usr/local/cargo /usr/local/cargo
RUN cargo build --release
# (5) run the app
FROM debian:bookworm-slim
COPY --from=builder /app/target/release/hello_world /usr/local/bin/hello_world
CMD hello_world
(1): install cargo-chef crate in a rust env.
(2): cargo chef checks your project and generates a recipe.json file which describes its skeleton. You can consider this file as Python's requirements.txt
(3): cargo chef builds and caches dependencies in release mode.
(4): copy the cache and build your app.
(5): copy the executable and run it in a minimal image.
Now if you update something in the application code, you can see that dependencies aren't built again.
And that's it !
Conclusion
cargo-chef provides an efficient way to avoid building dependencies again and again.
Here's one of my side project which takes advantage of cargo chef
Posted on March 15, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.