<h2>O Problema: Por Que Nossas Imagens Docker São Tão Grandes?</h2>
<p>Quando você constrói uma imagem Docker tradicional, está incluindo tudo na camada final: dependências de compilação, ferramentas de build, arquivos temporários, caches de gerenciadores de pacotes. Uma aplicação Go simples que deveria pesar 10 MB pode facilmente ocupar 500 MB ou mais. Uma aplicação Node.js fica ainda pior — você herda todas as dependências do <code>node_modules</code>, ferramentas de build como webpack, e tudo que foi instalado durante o desenvolvimento.</p>
<p>O impacto é real: imagens gigantescas significam mais tempo para fazer push/pull em registries, mais espaço em disco nos servidores, deploys mais lentos e custos maiores de armazenamento em plataformas cloud. Se você trabalha em ambientes com restrições de banda ou com centenas de deploys por dia, esse problema é um gargalo significativo.</p>
<h2>Entendendo Multi-stage Builds: O Conceito</h2>
<p>Multi-stage builds permitem usar múltiplos <code>FROM</code> em um único Dockerfile. Cada estágio é independente e você copia apenas os artefatos necessários do estágio anterior para o próximo. A imagem final contém apenas o estágio final — tudo que ficou nas etapas anteriores é descartado.</p>
<p>A ideia é separar o <strong>ambiente de build</strong> (pesado, com compiladores e ferramentas) do <strong>ambiente de runtime</strong> (leve, apenas o necessário para executar). Um paralelo na engenharia tradicional: você não distribui toda a fábrica junto com o produto final, apenas o produto.</p>
<h3>Como Funciona Internamente</h3>
<p>Quando Docker executa um multi-stage build, ele cria uma imagem intermediária para cada estágio. As camadas dos estágios anteriores não aparecem na imagem final — apenas as camadas do último <code>FROM</code> são preservadas. Você controla explicitamente o que passa de um estágio para outro via <code>COPY --from=<stage></code>.</p>
<p>O resultado é uma redução dramática de tamanho porque você deixa para trás compiladores, dependências de desenvolvimento e outros artefatos intermediários. Uma aplicação Go compilada pode ir de 800 MB para 15 MB; uma aplicação Python empacotada de 1.2 GB para 200 MB.</p>
<h2>Exemplo Prático 1: Aplicação Go</h2>
<h3>Sem Multi-stage (O Problema)</h3>
<pre><code class="language-dockerfile">FROM golang:1.21-alpine
WORKDIR /app
COPY . .
RUN go build -o myapp main.go
EXPOSE 8080
CMD ["./myapp"]</code></pre>
<p>Se você fizer o build: <code>docker build -t myapp:v1 .</code>, a imagem terá aproximadamente <strong>400 MB</strong> porque herda toda a imagem base <code>golang:1.21-alpine</code> (que inclui compilador, ferramentas de build, git, etc).</p>
<h3>Com Multi-stage (A Solução)</h3>
<pre><code class="language-dockerfile"># Estágio 1: Build
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY . .
RUN go build -o myapp main.go
Estágio 2: Runtime
FROM alpine:latest
WORKDIR /app
COPY --from=builder /app/myapp .
EXPOSE 8080
CMD ["./myapp"]</code></pre>
<p>Agora a imagem final terá apenas <strong>~15 MB</strong>. O estágio <code>builder</code> é descartado, levando consigo o compilador Go e todas as dependências de build. Apenas o binário compilado (<code>myapp</code>) é copiado para a imagem final baseada em <code>alpine:latest</code> (uma imagem mínima de ~7 MB).</p>
<p>Você constrói assim: <code>docker build -t myapp:v1 .</code> e obtém uma imagem pronta para produção, leve e segura.</p>
<h2>Exemplo Prático 2: Aplicação Node.js com Build</h2>
<h3>Sem Multi-stage</h3>
<pre><code class="language-dockerfile">FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
EXPOSE 3000
CMD ["node", "dist/index.js"]</code></pre>
<p>Imagem final: <strong>~800 MB</strong> (todos os devDependencies, cache do npm, ferramentas de build).</p>
<h3>Com Multi-stage</h3>
<pre><code class="language-dockerfile"># Estágio 1: Dependências e Build
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production && \
npm ci && \
npm run build
Estágio 2: Runtime
FROM node:18-alpine
WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package.json .
EXPOSE 3000
CMD ["node", "dist/index.js"]</code></pre>
<p>Imagem final: <strong>~180 MB</strong>. A diferença aqui é menor porque Node.js exige o runtime mesmo em produção, mas ainda eliminamos devDependencies e artefatos de build temporários.</p>
<p><strong>Versão ainda mais otimizada</strong> (usando distroless):</p>
<pre><code class="language-dockerfile">FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production && \
npm ci && \
npm run build
Usar distroless node para reduzir ainda mais
FROM gcr.io/distroless/nodejs18-debian11
WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package.json .
EXPOSE 3000
CMD ["dist/index.js"]</code></pre>
<p>Com distroless: <strong>~150 MB</strong> (não inclui shell, apt, ou qualquer ferrramenta desnecessária).</p>
<h2>Boas Práticas e Otimizações Avançadas</h2>
<h3>Nomenclatura de Estágios</h3>
<p>Use nomes descritivos em vez de índices numéricos. Fica mais legível e manutenível:</p>
<pre><code class="language-dockerfile">FROM golang:1.21-alpine AS builder
... build steps ...
FROM alpine:latest AS runtime
... runtime setup ...</code></pre>
<p>Quando você copia, faz: <code>COPY --from=builder</code> em vez de <code>COPY --from=0</code>.</p>
<h3>Otimizando Layers em Cada Estágio</h3>
<p>Combine comandos RUN quando possível para reduzir camadas:</p>
<pre><code class="language-dockerfile"># Ruim: múltiplas camadas
RUN apt-get update
RUN apt-get install -y curl git
RUN apt-get clean
Bom: uma camada
RUN apt-get update && \
apt-get install -y curl git && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*</code></pre>
<p>Cada <code>RUN</code> cria uma camada. Mesmo que você delete arquivos em um <code>RUN</code> subsequente, as camadas anteriores ocupam espaço. Por isso combine e limpe na mesma instrução.</p>
<h3>Selecionando a Imagem Base Correta</h3>
<p>Para runtime, escolha a menor imagem que satisfaz suas necessidades:</p>
<ul>
<li><strong><code>alpine</code></strong>: ~7 MB, minimalista, sem muitas ferramentas</li>
<li><strong><code>slim</code></strong>: ~150 MB para linguagens como Python/Node, mais compatível</li>
<li><strong><code>distroless</code></strong>: ~70-100 MB, otimizado para produção, sem shell</li>
<li><strong><code>scratch</code></strong>: ~0 MB, apenas para binários estáticos (Go)</li>
</ul>
<pre><code class="language-dockerfile"># Para Go: pode usar scratch se compilar estaticamente
FROM scratch
COPY --from=builder /app/myapp .
CMD ["./myapp"]</code></pre>
<p>Uma aplicação Go em <code>scratch</code> pode ter apenas <strong>~20 MB</strong> se tiver assets estáticos, ou até menos com apenas o binário.</p>
<h3>Cacheamento Inteligente</h3>
<p>Ordene as instruções do mais estável para o menos estável. Mudanças em <code>package.json</code> invalidam o cache, mas mudanças no código-fonte não deveriam invalidar a instalação de dependências:</p>
<pre><code class="language-dockerfile">FROM node:18-alpine AS builder
WORKDIR /app
Copiar dependências primeiro (muda raramente)
COPY package*.json ./
RUN npm ci
Copiar código depois (muda frequentemente)
COPY . .
RUN npm run build</code></pre>
<p>Assim, se você apenas mudar o código, o Docker reutiliza a camada de <code>npm ci</code> do cache.</p>
<h2>Conclusão</h2>
<p>Multi-stage builds são fundamentais para containerização profissional. Você reduz imagens de gigabytes para megabytes — e isso não é apenas um número, é a diferença entre deploys rápidos e lentos, entre sistemas escaláveis e sobrecarregados. A técnica é simples: use múltiplos <code>FROM</code>, construa em um estágio pesado e copie apenas o necessário para um estágio leve de runtime.</p>
<p>O segundo aprendizado crítico é que essa redução não sacrifica funcionalidade — seu aplicativo roda exatamente igual, mas mais rápido, com menos consumo de recursos e menor superfície de ataque (menos ferramentas = menos vulnerabilidades).</p>
<p>Por fim, combine multi-stage com imagens base apropriadas (alpine, distroless, scratch) e boas práticas de cache para alcançar o máximo de eficiência. Aplique isso hoje e você verá o impacto imediato em seus pipelines de CI/CD.</p>
<h2>Referências</h2>
<ul>
<li><a href="https://docs.docker.com/build/building/multi-stage/" target="_blank" rel="noopener noreferrer">Docker Official Documentation: Multi-stage builds</a></li>
<li><a href="https://github.com/GoogleContainerTools/distroless" target="_blank" rel="noopener noreferrer">Google Distroless Images</a></li>
<li><a href="https://kubernetes.io/docs/concepts/configuration/overview/#container-images" target="_blank" rel="noopener noreferrer">Kubernetes Best Practices: Container Images</a></li>
<li><a href="https://www.freecodecamp.org/news/the-docker-handbook/" target="_blank" rel="noopener noreferrer">The Docker Handbook - Farhan Hasin Chowdhury</a></li>
<li><a href="https://alpinelinux.org/about/" target="_blank" rel="noopener noreferrer">Alpine Linux Official Documentation</a></li>
</ul>
<p><!-- FIM --></p>