Introduction: Why Docker and Rust Are the Perfect Pair
Modern software development requires not only writing efficient code but also creating a reliable environment for its execution. Rust — a systems programming language known for its performance and memory safety — is ideal for building high-load services. Docker, in turn, provides lightweight and isolated containers that allow you to package an application with all its dependencies.
Combining these two technologies gives developers a powerful tool: a compiled Rust binary placed in a minimal container runs quickly, securely, and predictably on any platform. In this article, we will go from a simple example to a production-ready solution, examine common mistakes, and learn how to optimize images.
Basics: Creating a Simple Rust Application in Docker
Step 1. Preparing a Rust Project
First, let's create a simple web application in Rust using the popular framework actix-web. This is not mandatory, but it provides a clear example of working with the network inside a container.
Create a new project:
cargo new rust-docker-appcd rust-docker-appAdd the dependency to Cargo.toml:
[dependencies]actix-web = "4"Replace the contents of src/main.rs with:
use actix_web::{web, App, HttpServer, Responder};
async fn index() -> impl Responder { "Hello from Rust in Docker!"}
#[actix_web::main]async fn main() -> std::io::Result<()> { HttpServer::new(|| { App::new().route("/", web::get().to(index)) }) .bind("0.0.0.0:8080")? .run() .await}Step 2. A Simple Dockerfile
A basic Dockerfile for a Rust application looks like this:
FROM rust:latest
WORKDIR /appCOPY . .
RUN cargo build --release
EXPOSE 8080
CMD ["./target/release/rust-docker-app"]This approach works but has serious drawbacks: the image becomes very large (over 1 GB), and each rebuild re-downloads all dependencies.
Optimization: Multi-stage Build and Reducing Image Size
Multi-stage Build
A key technique for Rust developers is using a multi-stage build. The idea is simple: in the first stage, we use a full image with the compiler and tools, and in the second stage, we use a minimal image into which we copy only the finished binary.
# Stage 1: BuildFROM rust:latest AS builder
WORKDIR /appCOPY Cargo.toml Cargo.lock ./RUN mkdir src && echo "fn main() {}" > src/main.rsRUN cargo build --release 2>/dev/null || true
COPY src/ src/RUN cargo build --release
# Stage 2: Minimal imageFROM debian:bookworm-slim
WORKDIR /appCOPY --from=builder /app/target/release/rust-docker-app .
EXPOSE 8080CMD ["./rust-docker-app"]Why does this work? The first RUN cargo build caches the dependencies. When you change only the source code, Docker does not rebuild the dependencies from scratch, which speeds up the build significantly.
Using Alpine for Maximum Compression
If you need a minimal image size, use alpine as the final stage. Rust binaries are statically linked, so no additional libraries are required.
# Final stage with AlpineFROM alpine:latest
WORKDIR /appCOPY --from=builder /app/target/release/rust-docker-app .
EXPOSE 8080CMD ["./rust-docker-app"]The image size can then be as small as 5–15 MB instead of 1+ GB!
Advanced Techniques: Caching and Security
Caching Dependencies with sccache
For large projects, you can use