Introdução: por que um desenvolvedor Rust precisa do Docker?
Rust é conhecido por seu desempenho e segurança de memória, mas mesmo o binário mais eficiente requer um ambiente adequado para ser executado. O Docker resolve esse problema, empacotando a aplicação junto com todas as suas dependências em um contêiner isolado. Neste artigo, vamos abordar como containerizar aplicações Rust com Docker, usando técnicas modernas, incluindo multi-stage build e minimização do tamanho da imagem.
A containerização de projetos Rust é especialmente vantajosa porque Rust compila para um binário estático — isso permite criar imagens mínimas (alpine ou até scratch), que são executadas mais rapidamente e com mais segurança do que equivalentes em Python ou Node.js.
1. Fundamentos: Dockerfile para uma aplicação Rust simples
Vamos começar com um servidor web simples em Rust usando o framework actix-web. Criaremos um Dockerfile básico.
1.1. Estrutura do projeto
my_rust_app/
├── Cargo.toml
├── Cargo.lock
└── src/
└── main.rs
1.2. Exemplo de código (main.rs)
use actix_web::{get, App, HttpResponse, HttpServer, Responder};
#[get("/")]
async fn hello() -> impl Responder {
HttpResponse::Ok().body("Olá do contêiner Rust!")
}
#[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. Dockerfile simples
FROM rust:1.77 AS builder
WORKDIR /app
COPY Cargo.toml Cargo.lock ./
RUN mkdir src && echo "fn main() {}" > src/main.rs
RUN cargo build --release
COPY src/ src/
RUN cargo build --release
FROM debian:bookworm-slim
COPY --from=builder /app/target/release/my_rust_app /usr/local/bin/
EXPOSE 8080
CMD ["my_rust_app"]
Explicação: o primeiro estágio (builder) compila a aplicação, o segundo estágio copia apenas o binário para uma imagem Debian limpa. Isso reduz o tamanho de ~2 GB para ~100 MB.
2. Multi-stage build: otimização do tamanho da imagem
O multi-stage build é uma técnica chave para Rust. Ela permite separar o ambiente de compilação (com compilador, dependências) do ambiente de execução.
2.1. Dockerfile avançado com cache
Para acelerar compilações repetidas, armazenamos as dependências em cache:
FROM rust:1.77 AS builder
WORKDIR /app
COPY Cargo.toml Cargo.lock ./
RUN mkdir src && echo "fn main() {}" > src/main.rs
RUN cargo build --release 2>/dev/null || true
COPY src/ src/
RUN cargo build --release && \
cp target/release/my_rust_app /app/rust_app
FROM gcr.io/distroless/cc-debian12
COPY --from=builder /app/rust_app /app/rust_app
EXPOSE 8080
CMD ["/app/rust_app"]
Por que distroless? A imagem distroless contém apenas glibc e bibliotecas básicas — sem shell, apt, ls. Isso aumenta a segurança e reduz o tamanho para ~30 MB.
2.2. Imagem mínima com scratch
Se sua aplicação não usa libc (por exemplo, escrita com #![no_std]), você pode usar scratch:
FROM rust:1.77 AS builder
WORKDIR /app
COPY . .
RUN cargo build --release
FROM scratch
COPY --from=builder /app/target/release/my_rust_app /app
EXPOSE 8080
CMD ["/app"]
Essa imagem pesa menos de 10 MB!
3. Docker Compose: aplicação multicontêiner
Frequentemente, uma aplicação Rust trabalha em conjunto com PostgreSQL, Redis ou Nginx. Usamos docker-compose.yml:
3.1. Exemplo de 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
```