# cargo-chef: caching Rust dependencies in Docker layers

> cargo-chef splits a Rust build into a recipe and a cook step so Docker can cache compiled dependencies. It is a build-time tool for container images, not something to run on a working checkout.

**LukeMathWalker/cargo-chef** — A cargo-subcommand to speed up Rust Docker builds using Docker layer caching.

- Repository: https://github.com/LukeMathWalker/cargo-chef
- Stars: 2,712 · Forks: 146
- Language: Rust
- License: Apache-2.0
- Published: 2026-09-28 · Updated: 2026-09-28 · Language: en
- Canonical page: https://hysenlabs.com/projects/lukemathwalker-cargo-chef

## The problem cargo-chef solves in a Rust Docker build

A naive Rust Dockerfile copies the whole project and runs cargo build. Docker caches layers by content, so any edit to any source file invalidates the layer, and the next build recompiles every dependency from scratch. In a project with a few hundred crates that is minutes of CPU per commit, repeated on every branch and every push. The README frames the goal directly: cache the dependencies of your Rust project and speed up your Docker builds. The tool was written for the deployment chapter of Zero to Production In Rust and has since become a general-purpose build helper.

The audience is narrow and specific. This is for teams building container images in CI, where the same dependency graph is compiled over and over against a source tree that changes constantly. It is not a local build accelerator. The README carries a warning that cargo-chef is not meant to be run locally and that running it on an existing codebase can overwrite files, because its job is to run before the real source is copied in. If your build is a plain cargo build on a developer laptop, there is nothing here for you.

## How the recipe and cook split works

cargo-chef exposes two subcommands. prepare analyzes the current project and writes a recipe.json that captures the minimum set of files needed to build the dependencies: every Cargo.toml with its relative path, the Cargo.lock, and some extra metadata. The README notes one detail that matters in practice: prepare makes sure all libraries and binaries are explicitly declared in their manifests even when they sit at the canonical default locations, src/main.rs for a binary and src/lib.rs for a library. Without that normalization, two projects with identical dependency graphs but different file layouts would produce different recipes and lose cache hits.

The recipe is the only input cook needs. The README compares it to a Python requirements.txt. cook re-hydrates that skeleton and builds it, which is what populates the target directory with compiled dependencies. In a Dockerfile, the recipe is copied from a planner stage into a builder stage, cook runs there, and only then is the real source copied in and cargo build executed. The layer that runs cook is invalidated only when recipe.json changes, which in turn happens only when a manifest or the lockfile changes. Dependency edits are rare compared with source edits, so the expensive layer survives most commits.

The README claims speedups up to 5x measured on some commercial projects. That is the project's own figure, not an independent benchmark, and the actual gain depends on how much of your build time is dependency compilation versus your own crate. A workspace with a large dependency tree and a small application crate benefits most; a workspace where the application crate dominates the compile time will see less.

## Installing cargo-chef and running a first cook

The documented install path is crates.io. The --locked flag is part of the published command, and it pins the dependency versions cargo-chef itself was built against:

```bash
cargo install cargo-chef --locked
```

After that, cargo chef --help lists the two subcommands. The first real use is to generate a recipe from the root of a project:

```bash
cargo chef prepare --recipe-path recipe.json
```

You should end up with a recipe.json in the working directory. The README says the file can be inspected: it holds the manifests with their relative paths plus the lockfile. Then cook consumes it and compiles the dependencies:

```bash
cargo chef cook --recipe-path recipe.json
```

For a release build, add the flag before the recipe path, as the README shows:

```bash
cargo chef cook --release --recipe-path recipe.json
```

A different toolchain can be forced by prefixing the cargo invocation, for example cargo +nightly chef cook --recipe-path recipe.json. The intended setting is a Dockerfile with three stages. The README's example uses the pre-built image lukemathwalker/cargo-chef:latest-rust-1 as the chef stage, a planner stage that copies the source and runs prepare, and a builder stage that copies recipe.json across, runs cook, then copies the source and runs cargo build --release --bin app. A fourth stage based on debian:trixie-slim carries only the compiled binary. If you would rather not depend on the pre-built image, the README shows the alternative of running cargo install --locked cargo-chef inside a rust:1 stage, paying that install cost once and caching it from the second build onward.

## Toolchain pinning and the Alpine cross-compile caveat

Two constraints in the README are easy to miss and expensive to debug. The first is toolchain consistency: you must use the same Rust version in all stages, otherwise caching will not work as expected. A mismatch between the planner and builder stages is not a hard error, it is a silent loss of cache hits, which is worse because the build still succeeds and only the timing tells you something is wrong. The pre-built images encode this in the tag scheme, <cargo-chef version>-rust-<rust tag>, with aliases taken from the official Rust images such as 1, 1.93, 1.93.1, bookworm, slim-bookworm, bullseye, trixie, alpine3.23, and combinations like 1.93.1-bookworm. There are also latest-rust-<alias> tags and a latest tag that tracks the newest cargo-chef and the newest Rust. Note that the README's own alias list contains a typo, slim-bookworkm, which is worth knowing if you copy tags by hand.

The second constraint concerns Alpine. Running the binary under Alpine requires a fully static build, and the README recommends targeting x86_64-unknown-linux-musl with muslrust. cargo-chef works for that target, but the README is explicit that this is cross-compiling and the target toolchain must be specified explicitly. The sample Dockerfile starts from clux/muslrust:stable, switches to USER root, installs cargo-chef, and then carries the target flag through the cook and build steps. If you skip the explicit target, you get a build that either fails or produces a binary linked against the wrong libc.

## Where cargo-chef is the wrong tool

The README's own warning is the clearest limitation: cargo-chef is not meant to be run locally, and running prepare on an existing codebase can overwrite files. That rules out the tempting shortcut of generating a recipe once and committing it, or of running the tool inside a checkout you care about. Treat it as a container-build step only.

The caching is also coarse. It keys on the recipe, so anything that changes the recipe invalidates the expensive layer. Adding a dependency, bumping a version, or a lockfile update from a routine cargo update all trigger a full dependency recompile. In a repository with frequent dependency churn, the savings shrink considerably. There is no partial invalidation: cook rebuilds the skeleton it was handed.

Finally, the benefit is bounded by how your Docker build is structured. If your CI builds on a platform that does not preserve layer cache between runs, or if every build starts from a clean image, cargo-chef has nothing to reuse. It is a layer-caching strategy, and it is only as good as the cache underneath it. On a project where the whole compile takes under a minute, the extra planner stage and the recipe round-trip can cost more than they save.

## How cargo-chef compares with sccache and plain layer ordering

The closest alternative in the Rust ecosystem is sccache, which caches individual compiler invocations rather than Docker layers. The difference in approach is fundamental. sccache intercepts rustc calls and stores object files in a local or remote cache, so it can reuse work across different projects, different machines and different branch builds, as long as the compiler inputs match. It needs a cache backend and it changes how the compiler is invoked. cargo-chef does none of that: it manipulates the Dockerfile so that the existing Docker layer cache does the work, with no extra service and no compiler wrapper. If your problem is one image rebuilt repeatedly on the same builder, cargo-chef is the smaller change. If your problem is many builds across many machines, sccache addresses the part cargo-chef cannot, because Docker layer cache is local to the builder.

A third option is manual layer ordering: copy Cargo.toml and Cargo.lock first, create a dummy src/main.rs, run cargo build, then copy the real source. That achieves a similar effect without any tool, and it is what many projects did before cargo-chef existed. The difference is maintenance. The dummy-source trick has to be kept in sync with the actual binary and library names, and it breaks in workspaces with multiple crates. cargo-chef derives the skeleton from the manifests, which is why the README stresses that it normalizes default file locations. You are trading a dependency for not hand-maintaining the stub. The README does not discuss sccache or compare itself with it; that comparison is drawn from what each tool does, not from the project's documentation.

## Maintenance, licence and upgrade cost

The repository is not archived and the last push was on 2026-08-12, which is recent enough that the project is being worked on. Releases are frequent: v0.1.78 landed on 2026-04-13, and v0.1.77 and v0.1.76 both on 2026-03-03. The version is still 0.1.x, so the project has not committed to a stable API, and a release-plz configuration plus a CHANGELOG.md in the repository root suggest releases are automated and documented. Upgrading is cheap for the CLI itself: cargo install cargo-chef --locked picks up the newest version, and the pre-built image tags let you pin both cargo-chef and Rust independently. The real upgrade cost is in the Dockerfile, where the Rust tag has to move in every stage at once. Bumping only the builder stage silently kills the cache, which is the failure mode the README warns about.

On licensing, the Cargo.toml declares license = "Apache-2.0 OR MIT", and the repository carries both LICENSE-APACHE and LICENSE-MIT. That is a permissive dual licence, so you can choose either. The pre-built Docker image is a separate distribution channel and its own terms apply; the README does not state a licence for the image itself. If you redistribute the image inside your organisation, check that separately rather than assuming the crate licence covers it. This is not legal advice, and the README does not discuss the image's licensing at all.

## Conclusion

Adopt cargo-chef if you ship a Rust service or binary in a container and your CI spends most of its time recompiling unchanged crates. Skip it if your build is small enough that a plain cargo build layer is fine, if you cannot keep the Rust toolchain version identical across Docker stages, or if you are tempted to run it against a working checkout, which the README warns overwrites files. Before rolling it out, verify three things: that the same Rust tag appears in the chef, planner and builder stages, that the base image really has the tag you picked, and that a rebuild after a source-only change produces a cache hit on the cook layer. If the cook layer is still re-running after a no-op change, the recipe is churning and the caching will not pay for itself.

## FAQ

### What is cargo-chef?

It is a cargo subcommand that caches the dependencies of a Rust project to speed up Docker builds. It exposes two commands, prepare and cook, and is designed to run before the actual source code is copied into the image.

### How do I use cargo-chef?

Run cargo chef prepare --recipe-path recipe.json to generate a recipe, then cargo chef cook --recipe-path recipe.json to build the dependencies from it. In a Dockerfile this happens in separate stages, with cook placed before the source is copied so the layer stays cached.

### Is there an alternative to cargo-chef?

sccache caches individual compiler invocations instead of Docker layers, so it can reuse work across machines and branches, while cargo-chef relies on the local Docker layer cache. A manual Dockerfile that copies manifests first and builds a stub source achieves a similar effect without any tool. The README does not compare cargo-chef with either.

## Sources

- [Issues](https://github.com/LukeMathWalker/cargo-chef/issues)
- [License: Apache-2.0](https://github.com/LukeMathWalker/cargo-chef/blob/main/LICENSE)
- [LukeMathWalker/cargo-chef on GitHub](https://github.com/LukeMathWalker/cargo-chef)
- [README](https://github.com/LukeMathWalker/cargo-chef/blob/main/README.md)
- [Releases](https://github.com/LukeMathWalker/cargo-chef/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/lukemathwalker-cargo-chef
