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.

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
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:
- 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. - 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 insrc/. - 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.tomlorCargo.lock: the recipe changes, so dependencies recompile. This is correct. - Different
--releaseor--featuresbetween cook and build: use the same arguments in both. RUSTFLAGSor 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-toand--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
RUN --mount=type=cache,target=/usr/local/cargo/registry \
--mount=type=cache,target=/app/target \
cargo build --release --locked && cp target/release/myapp /myappBuildKit 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.


