Skip to content
rusthacks.dev
DevOps

Cargo Chef and Layer Caching for Faster Rust Image Builds

A Docker build that recompiles every dependency on each code change wastes minutes. cargo-chef splits dependency compilation into its own cached layer. See the Dockerfile and what invalidates it.

3 min read
Summarize in AI
Flat illustration for the post “Cargo Chef and Layer Caching for Faster Rust Image Builds”, composed around cargo.

docker build took six minutes after changing one line, because the Dockerfile copied the source before running cargo build, and Docker threw away the compiled dependencies. The standard remedy is to build dependencies in an earlier layer from files that change rarely. Doing this by hand means a dummy main.rs. cargo-chef automates it. This post gives the Dockerfile and the pitfalls. It extends a small Docker image for Rust and the CI caching in caching Cargo builds in CI.

Docker with BuildKit, cargo-chef 0.1, Rust 1.90.

The Dockerfile

DockerfileDockerfile
FROM rust:1.90-bookworm AS chef
RUN cargo install cargo-chef --locked
WORKDIR /app

FROM chef AS planner
COPY . .
RUN cargo chef prepare --recipe-path recipe.json

FROM chef AS builder
COPY --from=planner /app/recipe.json recipe.json
# Compiles only dependencies; cached until recipe.json changes
RUN cargo chef cook --release --recipe-path recipe.json
COPY . .
RUN cargo build --release --locked --bin myapp

FROM debian:bookworm-slim AS runtime
RUN useradd --system --uid 10001 app
COPY --from=builder /app/target/release/myapp /usr/local/bin/myapp
USER app
ENTRYPOINT ["/usr/local/bin/myapp"]

How it works:

  1. planner copies the whole source and writes recipe.json, a skeleton of the dependency graph (crate names, versions, features), with no source code in it.
  2. builder copies only the recipe, then cooks. This layer's input is recipe.json, so it is reused as long as the dependencies are the same, whatever you change in src/.
  3. Then the real source is copied and only your crate compiles.

The cargo install cargo-chef step is itself a layer; pin the version or use the prebuilt lukemathwalker/cargo-chef image to avoid compiling it each time.

What the second build looks like

Change a line in src/main.rs and rebuild: the planner stage reruns (cheap), produces an identical recipe.json, the cook layer is a cache hit, and only the final cargo build runs, compiling your crate. The time for that depends on your code size; the dependencies, usually the bulk, are skipped.

Things that bust the layer

  • Editing Cargo.toml or Cargo.lock: the recipe changes, so dependencies recompile. This is correct.
  • Different --release or --features between cook and build: use the same arguments in both.
  • RUSTFLAGS or a changed base image tag.
  • Not using BuildKit, or running on a CI runner with no layer cache. In CI, export and import the cache with --cache-to and --cache-from, or build on a runner that keeps its layers.

SQLx and build scripts

If you use SQLx offline mode, copy .sqlx/ and set SQLX_OFFLINE=true in the builder stage (SQLx offline mode). Crates whose build scripts need system libraries need those packages installed in the chef stage, since they compile during cook.

The alternative: cache mounts

Dockerfile
RUN --mount=type=cache,target=/usr/local/cargo/registry \
    --mount=type=cache,target=/app/target \
    cargo build --release --locked && cp target/release/myapp /myapp

BuildKit keeps target on the builder between builds, which also caches your own crate's incremental data. It does not survive on ephemeral CI runners unless you configure a cache backend. It needs the cp because the mount is not part of the image. Choose cargo-chef for portability and cache mounts for local speed.

Limits

Chef does not help the first build or a dependency bump. It does not shrink the runtime image, which is the job of the stages in base images compared. The tool's README documents the options: LukeMathWalker/cargo-chef.

Share

Keep reading

Get new posts by email

We send one email when a new post is published. No spam.

Unsubscribe in one click. No spam, ever.