Perfect Dockerfile for Rust backends

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 cc base 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 with scratch, the empty image.
  • Your Rust service probably does not handle signals like SIGINT. This means that if you use docker run and press Ctrl-C, your service won’t stop. The simplest solution is to use docker 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?


  1. 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. ↩︎


About the author: My name is Miikka Koskinen. I'm an experienced software engineer and consultant focused on solving problems in storing data in cloud: ingesting the data, storing it efficiently, scaling the processing, and optimizing the costs.

Could you use help with that? Get in touch at miikka@jacksnipe.fi.