Rust e Docker: Builds Otimizados para Produção — 2026

Aprenda a criar imagens Docker otimizadas para aplicações Rust com multi-stage builds, cargo-chef, cache de dependências e boas práticas de segurança.

Introdução

Compilar Rust dentro de um container Docker pode ser frustrante: builds de 15 minutos, imagens de 2 GB e cache que nunca funciona direito. Se você já passou por isso, este guia é para você. Vamos construir um pipeline Docker que gera imagens de menos de 10 MB, com builds incrementais rápidos e seguros para produção.

O segredo está em combinar multi-stage builds, cache inteligente de dependências e targets estáticos com musl. Essas técnicas são usadas por empresas como Cloudflare e Discord nos seus serviços Rust em produção.


O Problema: Build Ingênuo

O Dockerfile mais básico para Rust compila tudo em uma única imagem:

FROM rust:1.88
WORKDIR /app
COPY . .
RUN cargo build --release
CMD ["./target/release/meu-app"]

Esse approach tem três problemas graves:

  1. Imagem gigante (~1.4 GB): inclui o compilador Rust, todas as ferramentas de build e código-fonte
  2. Sem cache de dependências: qualquer mudança no código recompila todas as crates
  3. Superfície de ataque: ferramentas de desenvolvimento disponíveis em produção

Multi-Stage Build: A Base

A primeira otimização é separar o build da imagem final:

# Stage 1: Build
FROM rust:1.88-slim AS builder
WORKDIR /app
COPY . .
RUN cargo build --release

# Stage 2: Runtime
FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/*
COPY --from=builder /app/target/release/meu-app /usr/local/bin/
USER 1000
CMD ["meu-app"]

A imagem final cai de 1.4 GB para ~80 MB. Mas ainda recompilamos tudo a cada mudança no código. Vamos resolver isso.


Cache de Dependências com cargo-chef

O cargo-chef é a ferramenta padrão da comunidade para cache de dependências em Docker. Ele funciona em três etapas:

  1. Planner: analisa seu projeto e gera um “recipe” — um plano de build das dependências
  2. Cacher: compila apenas as dependências a partir do recipe
  3. Builder: compila seu código-fonte usando o cache das dependências
# Stage 1: Planner
FROM rust:1.88-slim AS planner
WORKDIR /app
RUN cargo install cargo-chef
COPY . .
RUN cargo chef prepare --recipe-path recipe.json

# Stage 2: Cache de dependências
FROM rust:1.88-slim AS cacher
WORKDIR /app
RUN cargo install cargo-chef
COPY --from=planner /app/recipe.json recipe.json
RUN cargo chef cook --release --recipe-path recipe.json

# Stage 3: Build
FROM rust:1.88-slim AS builder
WORKDIR /app
COPY --from=cacher /app/target target
COPY --from=cacher /usr/local/cargo /usr/local/cargo
COPY . .
RUN cargo build --release

# Stage 4: Runtime
FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/*
COPY --from=builder /app/target/release/meu-app /usr/local/bin/
USER 1000
CMD ["meu-app"]

Agora, quando você altera apenas código-fonte, o Docker reutiliza o cache do stage 2. O tempo de rebuild cai de 10+ minutos para segundos em muitos casos.

Usando com Cargo Workspaces

Se seu projeto usa Cargo workspaces, o cargo-chef funciona sem alterações. Ele detecta automaticamente a estrutura do workspace e gera o recipe correto para todos os crates.


Binários Estáticos com musl: Imagens Mínimas

Para imagens realmente pequenas, compile com o target x86_64-unknown-linux-musl e use scratch ou alpine como base:

# Stage 1: Build estático
FROM rust:1.88-slim AS builder
RUN apt-get update && apt-get install -y musl-tools && rm -rf /var/lib/apt/lists/*
RUN rustup target add x86_64-unknown-linux-musl

WORKDIR /app
COPY . .
RUN cargo build --release --target x86_64-unknown-linux-musl

# Stage 2: Imagem mínima
FROM scratch
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /app/target/x86_64-unknown-linux-musl/release/meu-app /
USER 1000
ENTRYPOINT ["/meu-app"]

O resultado? Uma imagem Docker de 5-10 MB contendo apenas seu binário e certificados SSL. Sem shell, sem package manager, sem nada que um atacante possa explorar.

Quando Não Usar musl

Nem toda crate compila limpo com musl. Projetos que dependem de linkagem dinâmica com bibliotecas C (como OpenSSL direto) podem ter problemas. Alternativas:

  • Use rustls em vez de openssl para TLS — funciona perfeitamente com musl
  • Para crates que usam SQLx com PostgreSQL, considere debian:bookworm-slim em vez de scratch
  • Se usar Diesel, compile com as features corretas para linkagem estática

Otimizações Avançadas de Build

Perfil de Release Otimizado

Configure seu Cargo.toml para gerar binários menores:

[profile.release]
opt-level = "z"      # Otimizar para tamanho
lto = true           # Link-Time Optimization
codegen-units = 1    # Compilação mais lenta, binário menor
panic = "abort"      # Remove overhead de unwinding
strip = true         # Remove símbolos de debug

Essas opções podem reduzir o binário de 20 MB para 3-5 MB. O trade-off é um tempo de compilação maior — perfeito para builds de CI/CD onde o tamanho da imagem importa.

Cache com BuildKit

O Docker BuildKit oferece cache de montagem que persiste entre builds:

FROM rust:1.88-slim AS builder
WORKDIR /app
COPY . .
RUN --mount=type=cache,target=/usr/local/cargo/registry \
    --mount=type=cache,target=/app/target \
    cargo build --release && \
    cp target/release/meu-app /usr/local/bin/meu-app

O diretório target/ e o registry do Cargo são preservados entre builds, proporcionando compilação verdadeiramente incremental.


Segurança em Produção

Usuário Não-Root

Nunca rode seu aplicativo como root. Use um usuário sem privilégios:

FROM debian:bookworm-slim
RUN groupadd -r appuser && useradd -r -g appuser appuser
COPY --from=builder --chown=appuser:appuser /app/target/release/meu-app /usr/local/bin/
USER appuser
CMD ["meu-app"]

Scanning de Vulnerabilidades

Integre scanning de vulnerabilidades no pipeline:

# Scan da imagem Docker
docker scout cve meu-app:latest

# Scan das dependências Rust
cargo audit
cargo deny check

O cargo audit verifica suas dependências contra o banco de dados RustSec. Combinado com tratamento de erros robusto e testes abrangentes, você tem uma aplicação sólida para produção.

Health Checks

Adicione health checks ao seu Dockerfile:

HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
    CMD ["/usr/local/bin/meu-app", "--health-check"]

Se sua aplicação usa Axum, implemente um endpoint /health:

use axum::{Router, routing::get};

async fn health_check() -> &'static str {
    "OK"
}

let app = Router::new()
    .route("/health", get(health_check));

Exemplo Completo: API Web com Axum

Juntando tudo — cargo-chef, musl, segurança — em um Dockerfile de produção:

FROM rust:1.88-slim AS planner
WORKDIR /app
RUN cargo install cargo-chef
COPY . .
RUN cargo chef prepare --recipe-path recipe.json

FROM rust:1.88-slim AS cacher
WORKDIR /app
RUN apt-get update && apt-get install -y musl-tools && rm -rf /var/lib/apt/lists/*
RUN rustup target add x86_64-unknown-linux-musl
RUN cargo install cargo-chef
COPY --from=planner /app/recipe.json recipe.json
RUN cargo chef cook --release --target x86_64-unknown-linux-musl --recipe-path recipe.json

FROM rust:1.88-slim AS builder
WORKDIR /app
RUN apt-get update && apt-get install -y musl-tools && rm -rf /var/lib/apt/lists/*
RUN rustup target add x86_64-unknown-linux-musl
COPY --from=cacher /app/target target
COPY --from=cacher /usr/local/cargo /usr/local/cargo
COPY . .
RUN cargo build --release --target x86_64-unknown-linux-musl

FROM scratch
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /app/target/x86_64-unknown-linux-musl/release/minha-api /
USER 1000
EXPOSE 3000
ENTRYPOINT ["/minha-api"]

Resultado: imagem de ~8 MB com build incremental rápido e superfície de ataque mínima.


Comparativo de Tamanho de Imagens

EstratégiaTamanho da ImagemTempo de Rebuild
Build simples (rust:1.88)~1.4 GB10-15 min
Multi-stage + Debian slim~80 MB10-15 min
Multi-stage + cargo-chef~80 MB~30 seg
musl + scratch~5-10 MB10-15 min
cargo-chef + musl + scratch~5-10 MB~30 seg

Perguntas Frequentes

Qual a melhor imagem base para Rust em produção?

Para tamanho mínimo e segurança máxima, use scratch com binários compilados para musl. Se sua aplicação precisa de ferramentas do sistema ou bibliotecas C dinâmicas, use debian:bookworm-slim.

O que é cargo-chef e por que usar?

cargo-chef é uma ferramenta que separa o build de dependências do build do seu código. Ele gera um “recipe” que permite ao Docker cachear a camada de dependências, evitando recompilações desnecessárias.

Posso usar Alpine Linux em vez de scratch?

Sim. Alpine usa musl por padrão, então binários compilados com o target musl rodam nativamente. A imagem final fica em torno de 10-15 MB — um pouco maior que scratch, mas com shell e package manager disponíveis para debugging.

Como debugar uma imagem scratch em produção?

Use docker debug (Docker Desktop) ou crie uma imagem de debug separada baseada em debian:bookworm-slim com ferramentas de diagnóstico. Mantenha a imagem de produção limpa e use logging com tracing para observabilidade.


Conclusão

Docker e Rust são uma combinação poderosa quando feitos direito. Com multi-stage builds, cargo-chef e targets estáticos, você consegue imagens de produção minúsculas e seguras, sem sacrificar a velocidade de desenvolvimento.

As técnicas deste artigo se aplicam igualmente a projetos simples e a monorepos com Cargo workspaces. Se você está montando um pipeline de CI/CD para Rust, combine essas práticas com testes automatizados para um fluxo de deploy confiável.

Para quem está explorando outras linguagens com Docker, compare as estratégias de containerização com Go (que também gera binários estáticos nativamente) e Python (onde o tamanho de imagem é um desafio maior).

Leia Também