Do you have a backend service implemented in Rust that you’re going to deploy in a Docker container? Not sure how to write the Dockerfile? Start by copy-pasting the one below.
# Customization point: set the name of the binary built by Cargo.
ARG BINARY_NAME=app
FROM rust:1-trixie AS chef
RUN --mount=type=cache,target=/usr/local/cargo/registry \
--mount=type=cache,target=/usr/local/cargo/git/db \
cargo install --locked cargo-chef
WORKDIR /build
FROM chef AS planner
COPY . .
RUN cargo chef prepare --recipe-path recipe.json
FROM chef AS builder
ARG BINARY_NAME
RUN mkdir -p /empty
# cargo-chef magic happens here: we build dependencies based only on `recipe.json`,
# so changes to source files do not retrigger dependency build
COPY --from=planner /build/recipe.json recipe.json
RUN --mount=type=cache,target=/usr/local/cargo/registry \
--mount=type=cache,target=/usr/local/cargo/git/db \
--mount=type=cache,target=/build/target \
cargo chef cook --release --recipe-path recipe.json
COPY . .
# We have to copy the binary out of `target` as the last step because it is a cache mount.
RUN --mount=type=cache,target=/usr/local/cargo/registry \
--mount=type=cache,target=/usr/local/cargo/git/db \
--mount=type=cache,target=/build/target \
cargo build --release --bin "$BINARY_NAME" && \
cp "target/release/$BINARY_NAME" app
################################################################################
FROM gcr.io/distroless/cc-debian13:nonroot
# Customization point: add labels you need, for example to indicate the repo location
# LABEL org.opencontainers.image.source=https://github.com/miikka/perfect-rust-dockerfile
WORKDIR /app
COPY --from=builder /build/app /app/app
# Customization point: add an empty directory owned by the nonroot user
# COPY --from=builder --chown=65532:65532 /empty /data
# Customization point: if you need files other than the binary, copy them into the image here.
# COPY static/ static/
# No `tini` here as the entrypoint, so unless your app handles signals explicitly, use
# `docker run --init` to ensure that Ctrl-C and SIGTERM work.
CMD ["/app/app"]
For a full Axum-based example project, see here.
I have a couple of Rust backends that get deployed in containers. I wanted to figure out once and for all what the Dockerfile should look like and this is the result.1
So, what’s so good about this? A couple of things:
- Caching the dependencies works. Thanks to cargo-chef, you don’t have to rebuild all your dependencies when you change your own source code only. This is a common painpoint in Rust builds inside Docker.
- Cache mounts are used for Cargo home so that the dependencies do not need to be redownloaded.
- The resulting image uses distroless base image. It does not contain a package manager or a shell. This means the image is smaller and you’ll have fewer alerts from the vulnerability scanners. The downside is that it sometimes makes debugging harder, but see the debug variants.
- Your app runs with a non-root user. This reduces the blast radius in any vulnerabilities your service might have.
What could be different?
- You should pin the base image versions with the
@sha256:<digest>syntax. It will prevent your application from breaking when the upstream image gets updated. I haven’t done it here because it makes the template easier to adopt, but you can do it with dockerfile-pin. - Now you need to use the
ccbase image to get glibc and libgcc, but you could do a fully static build, possibly with musl. Then you could use an even more minimalistic base image for the final image. You could get away withscratch, the empty image. - Your Rust service probably does not handle signals like SIGINT.
This means that if you use
docker runand press Ctrl-C, your service won’t stop. The simplest solution is to usedocker run --init. To avoid having to specify--init, you can implement signal handling in your service. Another solution is to include tini as the entrypoint and it will take care of the signal handling. I left it out for simplicity, but you might prefer it.
What would your perfect Dockerfile be like?
-
After writing this post, I realized that Claude Opus 4.7 will generate pretty much the same thing. Programming is over and so are this sort of blog posts. ↩︎