Introduction: Why does a Rust developer need Docker?
Rust is famous for its performance and memory safety, but even the most efficient binary file requires the correct environment to run. Docker solves this problem by packaging the application together with all its dependencies into an isolated container. In this article, we will break down how to containerize Rust applications using Docker, employing modern techniques, including multi-stage builds and minimizing image size.
Containerizing Rust projects is especially beneficial because Rust compiles into a static binary — this allows you to create minimal images (alpine or even scratch) that run faster and more securely than their Python or Node.js counterparts.
1. Basics: Dockerfile for a simple Rust application
Let's start with a simple web server in Rust using the actix-web framework. We'll create a basic Dockerfile.
1.1. Project structure
my_rust_app/├── Cargo.toml├── Cargo.lock└── src/ └── main.rs1.2. Code example (main.rs)
use actix_web::{get, App, HttpResponse, HttpServer, Responder};
#[get("/")]async fn hello() -> impl Responder { HttpResponse::Ok().body("Hello from Rust container!")}
#[actix_web::main]async fn main() -> std::io::Result<()> { HttpServer::new(|| App::new().service(hello)) .bind("0.0.0.0:8080")? .run() .await}1.3. Simple Dockerfile
FROM rust:1.77 AS builderWORKDIR /appCOPY Cargo.toml Cargo.lock ./RUN mkdir src && echo "fn main() {}" > src/main.rsRUN cargo build --releaseCOPY src/ src/RUN cargo build --release
FROM debian:bookworm-slimCOPY --from=builder /app/target/release/my_rust_app /usr/local/bin/EXPOSE 8080CMD ["my_rust_app"]Explanation: The first stage (builder) compiles the application, the second stage copies only the binary into a clean Debian image. This reduces the size from ~2 GB to ~100 MB.
2. Multi-stage build: optimizing image size
Multi-stage build is a key technique for Rust. It allows separating the build environment (with compiler, dependencies) from the runtime environment.
2.1. Advanced Dockerfile with caching
To speed up repeated builds, we cache the dependencies:
FROM rust:1.77 AS builderWORKDIR /appCOPY Cargo.toml Cargo.lock ./RUN mkdir src && echo "fn main() {}" > src/main.rsRUN cargo build --release 2>/dev/null || trueCOPY src/ src/RUN cargo build --release && \ cp target/release/my_rust_app /app/rust_app
FROM gcr.io/distroless/cc-debian12COPY --from=builder /app/rust_app /app/rust_appEXPOSE 8080CMD ["/app/rust_app"]Why distroless? The distroless image contains only glibc and basic libraries — without shell, apt, ls. This increases security and reduces the size to ~30 MB.
2.2. Minimal image on scratch
If your application does not use libc (for example, written with #![no_std]), you can use scratch:
FROM rust:1.77 AS builderWORKDIR /appCOPY . .RUN cargo build --release
FROM scratchCOPY --from=builder /app/target/release/my_rust_app /appEXPOSE 8080CMD ["/app"]Such an image weighs less than 10 MB!
3. Docker Compose: multi-container application
Often a Rust application works in conjunction with PostgreSQL, Redis or Nginx. We use docker-compose.yml:
3.1. Example docker-compose.yml
version: '3.8'services: app: build: . ports: - "8080:8080" environment: - DATABASE_URL=postgres://user:pass@db:5432/mydb - RUST_LOG=info depends_on: - db
db: image: postgres:16-alpine ```