Rust & Dockerfile : cache dependencies with cargo chef
Tags: rust, docker, devops, programming
Published: 2024-03-15 11:03:44

## Introduction

Let's consider this basic Dockerfile:

```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 :

```dockerfile
# 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](https://github.com/danieldidiobalsamo/url_shortener) which takes advantage of `cargo chef`