Rust Docker Builds That Recompile Everything and the Fix
Why a one-line change recompiles every crate in Docker, and the fixes in order of effort: copy order, a dummy main trick, cargo-chef, BuildKit cache mounts and a pinned base.

The build is slow for one reason: each time you change a source file, Docker restarts compilation at the earliest changed layer, and the earliest changed layer was COPY . .. Everything below it, including compiling 300 dependencies, runs again. The fix is to make the dependency compile depend on files that rarely change. This post gives three versions in increasing power, and a checklist of silent cache busters. It is a companion to cargo-chef.
Docker with BuildKit, Rust 1.90.
The failing Dockerfile
FROM rust:1.90-bookworm
WORKDIR /app
COPY . .
RUN cargo build --releaseAny change in the build context changes the COPY layer, so cargo build runs from cold, including dependencies. Run docker build --progress=plain . twice after editing a comment and watch the cargo output scroll.
Fix 1: manifests first, stub main
FROM rust:1.90-bookworm
WORKDIR /app
COPY Cargo.toml Cargo.lock ./
RUN mkdir src && echo 'fn main() {}' > src/main.rs \
&& cargo build --release --locked \
&& rm -rf src target/release/deps/myapp* target/release/myapp
COPY src ./src
RUN cargo build --release --lockedThe first cargo build compiles all dependencies against an empty main. That layer's inputs are the two manifests. After copying the real source, the second build only compiles your crate. Removing deps/myapp* forces Cargo to rebuild the real crate rather than think the stub is current. This works for one crate; in a workspace you must stub every member, which is where it gets brittle.
Fix 2: cargo-chef
Generates the recipe for you, handles workspaces and several binaries. The full Dockerfile is in cargo-chef and layer caching.
Fix 3: cache mounts
RUN --mount=type=cache,target=/usr/local/cargo/registry \
--mount=type=cache,target=/usr/local/cargo/git \
--mount=type=cache,target=/app/target \
cargo build --release --locked && cp target/release/myapp /usr/local/bin/myappBuildKit stores those directories outside the image layers and reuses them. Even your own crate benefits from incremental data. Limits: the cache lives on the builder, so it does not persist on throwaway CI runners without a cache backend, and target is not in the image, hence the cp.
Silent cache busters
- **No
.dockerignore.** Copying.gitortargetinto the context makes every commit invalidate layers. Addtarget,.gitandnode_modules. - **
ARGwith a changing value** above the cargo step, such as a build timestamp or commit SHA. Put it after the compile, in a later layer. - Unpinned base image tags.
rust:latestmoves, and every move rebuilds everything. Pinrust:1.90-bookworm. - Different flags.
--releasein one stage and not another, orRUSTFLAGSdifferences, produce separate artifacts. - **Copying
Cargo.lockafter the source**, or leaving it out ofCOPY.
Check that it works
Build, change one line in src/, build again, and read the plain progress output. The dependency layer should say CACHED. If it recompiles, find the first layer not marked cached; the line above it names what changed.
Limits
A dependency update still recompiles the changed crates and everything depending on them. For local development, you may prefer mounting the source into a container and running cargo there, which skips image builds entirely. The Docker documentation explains the rules in build cache invalidation.


