How to use Docker multi-stage builds to go from a 2GB Rust build image to a 12MB production image.
A naive Rust Dockerfile produces a ~2GB image because it includes the full Rust toolchain, cargo cache, and all build artifacts. You don't need any of that at runtime.
FROM rust:1.77
WORKDIR /app
COPY . .
RUN cargo build --release
CMD ["./target/release/my-app"]
# Image size: ~1.8GBKaranveer Singh Shaktawat
Full Stack Engineer & Infrastructure Architect
Building portfolio, contributing to open source, and seeking remote full-time roles with significant technical ownership.
Pick what you want to hear about — I'll only email when it's worth it.
Did this resonate?
One changed line in a Dockerfile invalidates every layer after it. Ordering your Dockerfile with this in mind cuts rebuild times dramatically.
Building Docsee — a cross-platform Docker management tool with a Tauri GUI and terminal TUI, and why Rust was the right choice.
# Stage 1: build
FROM rust:1.77-slim as builder
WORKDIR /app
# Cache dependencies separately from source
COPY Cargo.toml Cargo.lock ./
RUN mkdir src && echo "fn main() {}" > src/main.rs
RUN cargo build --release
RUN rm src/main.rs
# Now copy real source and build
COPY src ./src
RUN touch src/main.rs && cargo build --release
# Stage 2: runtime
FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y \
ca-certificates \
&& rm -rf /var/lib/apt/lists/*
COPY --from=builder /app/target/release/my-app /usr/local/bin/my-app
CMD ["my-app"]
# Image size: ~12MBThe COPY --from=builder instruction pulls only the compiled binary from the build stage. The Rust toolchain never touches the final image.
The two-step build (echo "fn main(){}" then overwrite) exploits Docker layer caching. Dependencies compile in a separate layer that only invalidates when Cargo.toml or Cargo.lock changes — not when you edit your source. On a project with 50+ dependencies, this saves 3-4 minutes per build when only source changes.
FROM rust:1.77 as builder
RUN rustup target add x86_64-unknown-linux-musl
WORKDIR /app
COPY . .
RUN cargo build --release --target x86_64-unknown-linux-musl
# Use scratch (empty image) for fully static binary
FROM scratch
COPY --from=builder /app/target/x86_64-unknown-linux-musl/release/my-app /my-app
CMD ["/my-app"]
# Image size: ~6MB (just the binary)scratch is Docker's empty base image. This only works if your binary has zero dynamic library dependencies — musl linking ensures that.
Used this for the DMARC report parser I built at Prachyam. The container went from ~1.9GB to 8MB.