O cargo-chef é uma das formas mais práticas de evitar que o Docker recompile todas as dependências de um projeto Rust a cada alteração em src/. O fluxo separa o build em três etapas: cargo chef prepare gera uma receita com os manifests, cargo chef cook compila as dependências em uma camada cacheável e cargo build compila o código da aplicação. Se Cargo.toml e Cargo.lock não mudarem, o Docker pode reaproveitar a etapa mais cara.
Para começar rapidamente, instale a ferramenta com cargo install cargo-chef --locked e use um Dockerfile com stages de planner, builder e runtime. O cargo-chef não promete acelerar o primeiro build nem substitui o cache do BuildKit ou o sccache; ele resolve um problema específico: tornar a camada de dependências estável mesmo quando os arquivos-fonte mudam.
Este guia explica como usar cargo-chef em Rust, como ele funciona com Cargo workspaces, quais arquivos invalidam o cache, como combinar a ferramenta com BuildKit e como decidir se ela realmente vale a pena no seu pipeline.
Por que o cache Docker comum funciona mal com Rust
Um Dockerfile ingênuo costuma copiar o repositório inteiro antes de compilar:
FROM rust:slim AS builder
WORKDIR /app
COPY . .
RUN cargo build --release
Cada instrução cria uma camada. O problema é que COPY . . muda quando qualquer arquivo do contexto muda. Uma alteração pequena em src/main.rs invalida essa camada e todas as seguintes, incluindo cargo build --release.
O Cargo possui seu próprio cache em target/ e nos diretórios de registry e Git, mas o build tradicional acima não oferece ao Docker uma fronteira estável entre dependências e código da aplicação. O resultado pode ser uma recompilação muito maior do que a mudança justificaria.
Em ecossistemas nos quais as dependências são declaradas em um único arquivo, é comum copiar primeiro o manifesto, instalar dependências e só depois copiar o código. Em Rust, reproduzir isso manualmente fica complicado porque um projeto pode incluir:
Cargo.tomleCargo.lockna raiz;- vários membros de um Cargo workspace;
- targets em
src/bin/; - exemplos, benches e testes;
- arquivos
build.rs; - dependências locais por
path; - configuração de features e profiles.
Criar arquivos-fonte fictícios para convencer o Cargo a compilar apenas dependências é possível, mas frágil. O cargo-chef automatiza essa representação.
Como o cargo-chef funciona
O fluxo possui dois comandos principais.
1. cargo chef prepare
O comando prepare examina a estrutura do projeto e grava uma receita:
cargo chef prepare --recipe-path recipe.json
O recipe.json descreve os manifests e os targets necessários para reproduzir a resolução do projeto sem depender do conteúdo real da aplicação. Esse arquivo vira a entrada estável da próxima camada.
2. cargo chef cook
O comando cook usa a receita para compilar as dependências:
cargo chef cook --release --recipe-path recipe.json
Depois disso, o Docker copia o repositório real e executa o build final:
cargo build --release
Como as dependências já foram compiladas na camada anterior, o build final tende a concentrar o trabalho nas crates modificadas do próprio projeto.
A ideia pode ser resumida assim:
| Etapa | Entrada principal | Quando o cache costuma ser invalidado |
|---|---|---|
prepare | Manifests e estrutura dos targets | Mudança em manifests, membros ou targets |
cook | recipe.json | Mudança na receita, toolchain, target ou flags |
cargo build | Código-fonte completo | Qualquer mudança relevante no código |
| runtime | Binário e arquivos necessários | Novo artefato produzido pelo builder |
O cargo-chef não é um serviço de cache. Ele reorganiza o build para que o mecanismo de layers do Docker consiga trabalhar melhor.
Dockerfile completo com cargo-chef
Abaixo está uma base para uma aplicação Rust que usa glibc e roda em Debian slim:
# syntax=docker/dockerfile:1
FROM rust:slim AS chef
RUN cargo install cargo-chef --locked
WORKDIR /app
FROM chef AS planner
COPY . .
RUN cargo chef prepare --recipe-path recipe.json
FROM chef AS builder
COPY --from=planner /app/recipe.json recipe.json
RUN cargo chef cook --release --recipe-path recipe.json
COPY . .
RUN cargo build --release --locked --bin minha-api
FROM debian:bookworm-slim AS runtime
RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY --from=builder /app/target/release/minha-api /usr/local/bin/minha-api
USER 10001:10001
ENTRYPOINT ["/usr/local/bin/minha-api"]
Substitua minha-api pelo nome real do binário. Para um package com nome diferente do executável, confira as seções [[bin]] do Cargo.toml ou use cargo metadata.
Alguns detalhes desse exemplo são importantes:
cargo-chefé instalado uma vez no stage-basechef;- planner e builder usam a mesma imagem e a mesma toolchain;
- somente
recipe.jsonentra antes decargo chef cook; - o código real é copiado depois da camada de dependências;
--lockedimpede uma resolução diferente da registrada no lockfile;- a imagem final não contém
rustc, Cargo nem o código-fonte; - a aplicação não roda como root.
Para uma visão mais ampla sobre imagem final, musl, certificados e multi-stage build, consulte o guia de Rust com Docker em produção.
Crie um .dockerignore antes de medir
Um contexto de build desnecessariamente grande deixa o envio para o daemon mais lento e pode invalidar layers por arquivos irrelevantes. Um ponto de partida:
.git
.github
.gitea
target
Dockerfile*
docker-compose*.yml
.env
.env.*
*.log
README.md
Não copie essa lista sem revisar. Se o build usa um arquivo excluído — uma migration SQL embutida, um asset, um certificado de desenvolvimento ou metadata gerada — ele precisa continuar disponível.
O item mais importante para projetos Rust costuma ser target. Copiar o target/ local para o contexto produz imagens maiores, diferenças entre máquinas e invalidações difíceis de entender. Deixe o stage builder criar seus próprios artefatos.
Também não inclua segredos no contexto. Tokens para registry privado devem entrar por mecanismos de secret do BuildKit ou da plataforma de CI, nunca por COPY .env.
cargo-chef com Cargo workspaces
Em um workspace, execute o planner na raiz que contém [workspace]:
FROM chef AS planner
COPY . .
RUN cargo chef prepare --recipe-path recipe.json
No builder, compile o package desejado explicitamente:
RUN cargo chef cook \
--release \
--locked \
--recipe-path recipe.json
COPY . .
RUN cargo build --release --locked --package api --bin api
Há duas estratégias possíveis:
- cozinhar o workspace inteiro: maximiza o reaproveitamento quando vários packages são construídos na mesma imagem, mas pode compilar dependências desnecessárias;
- cozinhar para um package ou conjunto específico: reduz trabalho, porém exige que as opções de
cooke do build final estejam alinhadas.
O ponto crítico é manter as mesmas dimensões de compilação nas duas etapas:
- profile (
--releaseou outro profile); - target (
--target); - package e workspace;
- features;
--no-default-features;- toolchain;
- flags passadas por
RUSTFLAGS; - variáveis observadas por scripts de build.
Se você executa cargo chef cook --release, mas depois chama cargo build --release --no-default-features --features postgres, parte das dependências pode precisar ser recompilada porque o grafo efetivo mudou.
Features, targets e cross-compilation
O comando cook deve receber as mesmas features usadas no build final:
ARG FEATURES="postgres,rustls"
RUN cargo chef cook \
--release \
--locked \
--no-default-features \
--features "${FEATURES}" \
--recipe-path recipe.json
COPY . .
RUN cargo build \
--release \
--locked \
--no-default-features \
--features "${FEATURES}" \
--bin minha-api
Para cross-compilation, repita também o target:
RUN rustup target add x86_64-unknown-linux-musl
RUN cargo chef cook \
--release \
--target x86_64-unknown-linux-musl \
--recipe-path recipe.json
COPY . .
RUN cargo build \
--release \
--target x86_64-unknown-linux-musl \
--bin minha-api
Adicionar o target ao Rust não instala automaticamente todas as ferramentas nativas necessárias. Crates com código C, geração de bindings, OpenSSL ou bibliotecas do sistema podem exigir compilador, linker, headers e packages específicos. O guia de cross-compilation em Rust ajuda a separar problemas de cache de problemas do toolchain.
cargo-chef vs sccache vs BuildKit
As três soluções são complementares:
| Ferramenta | Nível de atuação | Principal benefício |
|---|---|---|
| cargo-chef | Estrutura das layers Docker | Mantém dependências em uma camada separada do código |
| BuildKit cache mount | Diretórios usados durante o build | Preserva registry, Git checkout e target entre execuções |
| sccache | Invocações do compilador | Reutiliza resultados de compilação compatíveis |
| cache remoto da CI | Exportação das layers ou mounts | Reaproveita cache entre runners efêmeros |
Um cache mount pode ser usado assim:
RUN --mount=type=cache,target=/usr/local/cargo/registry \
--mount=type=cache,target=/usr/local/cargo/git \
cargo chef cook --release --locked --recipe-path recipe.json
No build final:
RUN --mount=type=cache,target=/usr/local/cargo/registry \
--mount=type=cache,target=/usr/local/cargo/git \
--mount=type=cache,target=/app/target \
cargo build --release --locked --bin minha-api \
&& cp /app/target/release/minha-api /tmp/minha-api
Se target é um mount, o artefato precisa ser copiado para um caminho fora do mount dentro da mesma instrução, como /tmp/minha-api, antes de ser transferido ao runtime stage. Caso contrário, a próxima etapa pode não encontrar o binário na camada resultante.
O sccache resolve outra parte do problema. Configure RUSTC_WRAPPER=sccache e um backend adequado quando o pipeline precisa reutilizar objetos de compilação entre branches ou máquinas. Leia o guia de como reduzir o tempo de compilação do Rust para escolher entre linkador rápido, cache, Cranelift e redução de features.
Exemplo com cache do GitHub Actions
O BuildKit consegue importar e exportar cache pela própria action de build:
name: container
on:
push:
branches: [main]
jobs:
image:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- uses: docker/build-push-action@v6
with:
context: .
push: false
tags: minha-api:${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max
Em um deploy real, configure autenticação e push: true para o registry escolhido. Mantenha permissões mínimas e não coloque credenciais no Dockerfile.
O cargo-chef melhora a disposição das layers; cache-from e cache-to permitem que essas layers sobrevivam ao runner efêmero. Sem exportação remota, um runner descartável pode começar sem nenhum cache, independentemente da qualidade do Dockerfile.
Para uma pipeline completa com lint, testes e artefatos, veja CI/CD para Rust. Se a suíte é grande, o guia de cargo-nextest mostra como separar a otimização dos testes da otimização do build da imagem.
O que invalida o cache
É normal que a camada de cook seja refeita quando algo muda no grafo de compilação. Entre os gatilhos mais comuns estão:
- alteração em
Cargo.tomlouCargo.lock; - entrada, saída ou renomeação de um membro do workspace;
- mudança em features habilitadas;
- troca da imagem-base ou da versão do Rust;
- mudança do target ou profile;
- alteração em
RUSTFLAGS; - mudança em dependência local por
path; - modificação da estrutura de targets, como novo
src/bin/*.rs; - scripts
build.rssensíveis a arquivos ou variáveis externas; - uso de argumentos Docker diferentes antes da camada de
cook.
Não tente impedir toda invalidação. Se uma dependência mudou, recompilá-la é correto. O objetivo é evitar invalidações causadas apenas por uma alteração em código que não muda o grafo de dependências.
Use este diagnóstico:
docker build --progress=plain -t minha-api:test .
Faça uma segunda execução sem mudanças e observe quais etapas aparecem como CACHED. Depois altere somente um arquivo em src/ e repita. A etapa cargo chef cook deve continuar cacheada; o build final deve rodar novamente.
Armadilhas comuns
Instalar cargo-chef duas vezes
Se planner e builder começam de imagens independentes e ambos executam cargo install cargo-chef, você paga a compilação da ferramenta duas vezes. Crie um stage-base chef e derive os dois dele, como no exemplo principal.
Usar versões ou toolchains diferentes
Planner e builder devem operar em ambientes compatíveis. Trocar a imagem Rust entre etapas pode invalidar artefatos ou produzir recompilações. Fixe a estratégia de atualização da toolchain e faça o bump de forma intencional.
Esquecer –locked
Sem --locked, o Cargo pode resolver versões diferentes quando o lockfile não está presente ou não é respeitado. Para aplicações, preserve Cargo.lock e use --locked no cook e no build final.
Copiar o código antes de cook
Se o builder recebe COPY . . antes de cargo chef cook, uma mudança em src/ invalida justamente a layer que deveria ser estável. Antes do cook, copie apenas o recipe.json vindo do planner.
Esperar ganho no primeiro build
O primeiro build ainda baixa e compila dependências. Em alguns casos ele fica um pouco mais lento pela preparação da receita. Meça pelo cenário real: vários commits alterando código com manifests relativamente estáveis.
Cachear target sem estratégia
O diretório target pode crescer muito e conter artefatos incompatíveis entre toolchains, targets e conjuntos de flags. Defina chaves de cache adequadas, limite retenção e não trate todo target/ como um pacote universal.
Quando usar e quando não usar
cargo-chef tende a valer a pena quando:
- a aplicação possui muitas dependências;
- imagens são reconstruídas várias vezes por dia;
- a maior parte dos commits altera
src/, não manifests; - o pipeline usa cache persistente de layers;
- há um workspace com serviços ou binários recorrentes;
- o tempo de build afeta diretamente o ciclo de deploy.
Talvez não valha a complexidade quando:
- a crate é pequena e compila rapidamente;
- cada build começa sem cache local ou remoto;
Cargo.tomleCargo.lockmudam em quase todo commit;- a imagem é construída raramente;
- o pipeline já usa um cache de compilação eficaz e o gargalo está em testes, link ou download da imagem-base;
- a equipe ainda não possui um Dockerfile multi-stage correto.
Uma boa regra é começar com multi-stage build e .dockerignore, medir, habilitar cache remoto do BuildKit e só então adicionar cargo-chef se a compilação das dependências continuar dominando os rebuilds.
Checklist para produção
Antes de publicar a imagem, confira:
- planner e builder usam a mesma família de imagem e toolchain;
-
cargo-chefestá disponível emprepareecook; -
Cargo.lockestá versionado e--lockedé usado; - features, target e profile coincidem entre
cookebuild; -
target/,.git/e segredos não entram no contexto; - cache remoto está configurado para runners efêmeros;
- o binário é copiado para uma imagem final sem compilador;
- certificados e bibliotecas dinâmicas necessárias existem no runtime;
- o processo roda sem privilégios de root;
- testes e auditorias acontecem antes do push da imagem;
- a imagem é identificada por tag imutável ou digest no deploy;
- tempos de build são medidos em vez de estimados.
Para segurança de dependências e políticas de licença, combine o pipeline com cargo-deny, RustSec e SBOM. Para organizar publicação de binários e checksums, consulte engenharia de release em Rust.
Conclusão
O cargo-chef funciona porque cria uma fronteira de cache que o Docker não consegue inferir sozinho em projetos Rust complexos. prepare transforma manifests e estrutura em recipe.json; cook compila as dependências; e o build final recebe o código real. Em rebuilds com dependências estáveis, a etapa cara pode ser reaproveitada.
A ferramenta não deve ser instalada por hábito. Primeiro corrija o Dockerfile multi-stage, reduza o contexto com .dockerignore e garanta que a CI exporta cache. Depois compare builds frios e quentes. Se a camada de dependências permanece dominante, cargo-chef é uma solução clara, reproduzível e fácil de explicar para o time.
Dominar esse fluxo é uma competência útil para quem trabalha com backend, plataforma e DevOps em Rust. Consulte também as vagas de Rust e os perfis de empresas que usam Rust para entender onde conhecimentos de containers, CI e performance de build aparecem no mercado.
Perguntas frequentes
O que é cargo-chef?
cargo-chef é uma ferramenta que gera uma receita dos manifests de um projeto Rust e permite compilar dependências em uma layer Docker separada. Isso evita refazer todo o grafo quando apenas o código da aplicação muda.
Como instalar cargo-chef?
Use:
cargo install cargo-chef --locked
Em Docker, prefira instalar uma vez em um stage-base compartilhado pelo planner e pelo builder.
cargo-chef substitui sccache?
Não. cargo-chef organiza layers; sccache reutiliza resultados do compilador. É possível usar os dois, desde que o ganho justifique a operação e o armazenamento adicionais.
cargo-chef funciona com workspaces?
Sim. Rode prepare na raiz do workspace e mantenha alinhados packages, features, target e profile entre cook e o build final.
Por que cargo chef cook recompila tudo?
As causas mais comuns são mudança no recipe.json, toolchain diferente, features ou target desalinhados, ausência de cache persistente e argumentos Docker que invalidam a layer. Execute o build com --progress=plain para identificar exatamente onde o cache foi perdido.