DevOps & CI/CD

O que Todo Dev Deve Saber sobre Dockerfile Avançado: Multi-stage Builds, Distroless e Boas Práticas

12 min de leitura

O que Todo Dev Deve Saber sobre Dockerfile Avançado: Multi-stage Builds, Distroless e Boas Práticas

Entendendo Docker Multi-stage Builds Um dos maiores desafios ao containerizar aplicações é o tamanho final da imagem Docker. Quando você constrói uma imagem de forma tradicional, todos os artefatos do processo de build — dependências de desenvolvimento, compiladores, ferramentas auxiliares — acabam sendo inclusos na imagem final. Isso torna a imagem desnecessariamente grande e aumenta o tempo de download, consumo de banda e até riscos de segurança. Multi-stage builds resolvem exatamente isso. A ideia é usar múltiplos estágios (stages) em um único Dockerfile: em alguns você faz o build completo com todas as ferramentas necessárias, e apenas no estágio final você copia os artefatos realmente necessários para executar a aplicação. Pense nisso como usar um canteiro de obras para construir uma casa, mas depois remover o canteiro antes de entregar a propriedade. Conceito Prático de Estágios Cada estágio em um Dockerfile começa com e recebe um alias opcional via . Você pode referenciar um estágio anterior usando . Os estágios

<h2>Entendendo Docker Multi-stage Builds</h2>

<p>Um dos maiores desafios ao containerizar aplicações é o tamanho final da imagem Docker. Quando você constrói uma imagem de forma tradicional, todos os artefatos do processo de build — dependências de desenvolvimento, compiladores, ferramentas auxiliares — acabam sendo inclusos na imagem final. Isso torna a imagem desnecessariamente grande e aumenta o tempo de download, consumo de banda e até riscos de segurança.</p>

<p>Multi-stage builds resolvem exatamente isso. A ideia é usar múltiplos estágios (stages) em um único Dockerfile: em alguns você faz o build completo com todas as ferramentas necessárias, e apenas no estágio final você copia os artefatos realmente necessários para executar a aplicação. Pense nisso como usar um canteiro de obras para construir uma casa, mas depois remover o canteiro antes de entregar a propriedade.</p>

<h3>Conceito Prático de Estágios</h3>

<p>Cada estágio em um Dockerfile começa com <code>FROM</code> e recebe um alias opcional via <code>AS</code>. Você pode referenciar um estágio anterior usando <code>COPY --from=nome-do-estagio</code>. Os estágios anteriores ao final são descartados da imagem final, mantendo apenas a última camada. Isso é fundamental para reduzir o tamanho.</p>

<p>Veja um exemplo prático com uma aplicação Go:</p>

<pre><code class="language-dockerfile"># Estágio 1: Build

FROM golang:1.21-alpine AS builder

WORKDIR /app

COPY go.mod go.sum ./

RUN go mod download

COPY . .

RUN CGO_ENABLED=0 GOOS=linux go build -o app .

Estágio 2: Runtime

FROM alpine:3.18

WORKDIR /app

COPY --from=builder /app/app .

EXPOSE 8080

CMD [&quot;./app&quot;]</code></pre>

<p>Aqui, a imagem <code>golang:1.21-alpine</code> com todas as ferramentas de build fica apenas no estágio <code>builder</code>. A imagem final usa apenas <code>alpine:3.18</code> (muito mais leve) e copia apenas o executável compilado. O resultado é uma imagem muitas vezes menor.</p>

<h3>Vantagens e Casos de Uso</h3>

<p>Multi-stage é especialmente útil em linguagens compiladas como Go, Rust, C# e Java. Porém, também funciona bem com JavaScript/Node quando você precisa fazer build de ativos (minificação, bundling). O padrão gera imagens menores, reduz a superfície de ataque removendo ferramentas desnecessárias e acelera deploys. Use múltiplos estágios sempre que seu processo de build for significativamente diferente do runtime.</p>

<h2>Imagens Distroless: Minimalismo de Verdade</h2>

<p>Distroless é um conceito criado pelo Google que vai além de Alpine. Enquanto Alpine reduz o tamanho usando uma distribuição Linux minimalista, distroless remove até o gerenciador de pacotes e ferramentas do shell. A imagem contém apenas sua aplicação e as bibliotecas C necessárias para executá-la. Literalmente: sem bash, sem apt, sem nada além do essencial.</p>

<p>A filosofia por trás é simples: se você não usa uma ferramenta em produção, por que tê-la? Menos código significa menos vulnerabilidades e menos superfície de ataque. Uma imagem distroless típica pesa entre 5MB a 50MB, enquanto uma Alpine com a mesma aplicação pode pesar 100MB+ e uma imagem tradicional pode chegar a 1GB.</p>

<h3>Utilizando Imagens Distroless</h3>

<p>O Google mantém imagens base distroless no Google Container Registry. Existem variantes para diferentes linguagens: base, nodejs, python, java, cc (C/C++). Para começar, você as importa normalmente no <code>FROM</code> dentro de um multi-stage.</p>

<p>Aqui está um exemplo com Node.js:</p>

<pre><code class="language-dockerfile"># Estágio 1: Build

FROM node:18-alpine AS builder

WORKDIR /app

COPY package*.json ./

RUN npm ci --only=production

Estágio 2: Runtime com Distroless

FROM gcr.io/distroless/nodejs18-debian11

WORKDIR /app

COPY --from=builder /app/node_modules ./node_modules

COPY . .

EXPOSE 3000

CMD [&quot;index.js&quot;]</code></pre>

<p>Neste exemplo, a imagem final contém apenas Node.js e suas dependências. Sem npm, sem git, sem ferramentas de sistema. Se você precisar debugar em produção, não conseguirá fazer exec e abrir um bash — e isso é exatamente o ponto. Força você a ter logs estruturados e monitoring adequados desde o início.</p>

<h3>Quando e Por Que Usar Distroless</h3>

<p>Use distroless em produção para aplicações que já estão maduras e bem testadas. Para desenvolvimento e CI/CD intermediário, Alpine ou até imagens maiores fazem sentido, pois você vai precisar de ferramentas para troubleshooting. Distroless é perfeito para microsserviços que rodam em Kubernetes e precisam ser rápidos de deployar e seguros.</p>

<p>Uma desvantagem: debugar é mais difícil. Se a aplicação crashar com um erro silencioso, você não terá shell para investigar. Por isso, logs são críticos. Também requer que sua aplicação funcione completamente em runtime — não há espaço para hacks do tipo &quot;instalar um pacote na mão&quot;.</p>

<h2>Boas Práticas em Dockerfile Avançado</h2>

<p>Agora que você conhece multi-stage e distroless, precisa aprender a combiná-los com práticas sólidas. Um Dockerfile bem escrito não é apenas sobre tamanho; é sobre reprodutibilidade, segurança e performance.</p>

<h3>Ordem de Camadas e Cache</h3>

<p>Docker constrói imagens em camadas. Cada comando (<code>RUN</code>, <code>COPY</code>, <code>ADD</code>) cria uma camada. Se você mudar um arquivo, todas as camadas depois dele precisam ser reconstruídas. Por isso, a ordem importa. Coloque instruções que mudam raramente antes das que mudam frequentemente.</p>

<p>Exemplo inadequado:</p>

<pre><code class="language-dockerfile">FROM node:18-alpine

WORKDIR /app

COPY . .

RUN npm install

CMD [&quot;npm&quot;, &quot;start&quot;]</code></pre>

<p>Se você alterar um arquivo <code>.js</code>, npm install será executado novamente — desperdício. Melhor assim:</p>

<pre><code class="language-dockerfile">FROM node:18-alpine

WORKDIR /app

COPY package*.json ./

RUN npm ci --only=production

COPY . .

CMD [&quot;npm&quot;, &quot;start&quot;]</code></pre>

<p>Aqui, se apenas <code>.js</code> mudar, a camada de <code>npm ci</code> é reutilizada do cache. Ganho de tempo significativo em builds iterativos.</p>

<h3>Princípio do Menor Privilégio</h3>

<p>Nunca execute sua aplicação como <code>root</code>. Crie um usuário específico:</p>

<pre><code class="language-dockerfile">FROM gcr.io/distroless/nodejs18-debian11

RUN groupadd -r appuser &amp;&amp; useradd -r -g appuser appuser

WORKDIR /app

COPY --chown=appuser:appuser --from=builder /app/node_modules ./node_modules

COPY --chown=appuser:appuser . .

USER appuser

EXPOSE 3000

CMD [&quot;index.js&quot;]</code></pre>

<p>Isso reduz o risco se sua aplicação for comprometida. Um atacante não terá privilégios root para danificar o host. Com distroless, esse usuário ainda não terá acesso a shell, aumentando a segurança ainda mais.</p>

<h3>Saúde da Aplicação e Healthcheck</h3>

<p>Inclua <code>HEALTHCHECK</code> para que orquestradores como Kubernetes saibam se seu container está vivo e respondendo:</p>

<pre><code class="language-dockerfile">FROM node:18-alpine

WORKDIR /app

COPY package*.json ./

RUN npm ci --only=production

COPY . .

EXPOSE 3000

HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \

CMD node -e &quot;require(&#039;http&#039;).get(&#039;http://localhost:3000/health&#039;, (r) =&gt; {if (r.statusCode !== 200) throw new Error(r.statusCode)})&quot;

CMD [&quot;npm&quot;, &quot;start&quot;]</code></pre>

<p>Assim, se a aplicação travar ou ficar irresponsiva, o container será automaticamente marcado como unhealthy. Em produção, isso força um restart ou remoção do container.</p>

<h3>Segurança: Scanning e Imagens Conhecidas</h3>

<p>Sempre use tags específicas, nunca <code>latest</code>. Escaneie suas imagens em busca de vulnerabilidades usando ferramentas como Trivy ou o próprio Docker Scout:</p>

<pre><code class="language-bash">docker scout cves seu-app:1.0.0</code></pre>

<p>Além disso, use apenas imagens base de fontes confiáveis. Distroless do Google e imagens oficiais de linguagens (no Docker Hub) são seguras. Verifique assinaturas se possível.</p>

<h3>Exemplo Completo: Aplicação Python com FastAPI</h3>

<p>Aqui está um exemplo real combinando tudo — multi-stage, distroless, boas práticas:</p>

<pre><code class="language-dockerfile"># Estágio 1: Builder

FROM python:3.11-slim AS builder

WORKDIR /app

COPY requirements.txt .

RUN pip install --user --no-cache-dir -r requirements.txt

Estágio 2: Runtime com Distroless

FROM gcr.io/distroless/python3.11-debian11

COPY --from=builder /root/.local /home/appuser/.local

COPY --chown=nobody:nogroup . /app

WORKDIR /app

ENV PATH=/home/appuser/.local/bin:$PATH

ENV PYTHONUNBUFFERED=1

USER nobody

EXPOSE 8000

CMD [&quot;uvicorn&quot;, &quot;main:app&quot;, &quot;--host&quot;, &quot;0.0.0.0&quot;, &quot;--port&quot;, &quot;8000&quot;]</code></pre>

<p>Este Dockerfile garante: imagem pequena (apenas Python + dependências), sem acesso a shell, sem root, execução direta da aplicação. Uma imagem assim pode pesar 200-300MB em vez de 1GB+ com uma abordagem tradicional.</p>

<h3>Variáveis de Ambiente e Configuração</h3>

<p>Exporte configurações relevantes mas sensíveis como <code>ARG</code> durante build e <code>ENV</code> em runtime:</p>

<pre><code class="language-dockerfile">FROM gcr.io/distroless/nodejs18-debian11

ARG NODE_ENV=production

ENV NODE_ENV=$NODE_ENV

ENV LOG_LEVEL=info

COPY . /app

WORKDIR /app

CMD [&quot;index.js&quot;]</code></pre>

<p>Isso permite que você customize a imagem sem reconstruir, passando <code>--build-arg NODE_ENV=development</code> ao fazer build. Em runtime, qualquer consumidor da imagem vê que <code>LOG_LEVEL</code> é configurável.</p>

<h2>Conclusão</h2>

<p>Você agora domina três pilares essenciais de Docker avançado. Primeiro: <strong>multi-stage builds eliminam artefatos desnecessários</strong>, reduzindo tamanho de imagem drasticamente e acelerando deploys. Segundo: <strong>distroless leva minimalismo ao extremo</strong>, removendo toda a complexidade desnecessária e aumentando segurança por simplicidade. Terceiro: <strong>boas práticas como ordem de layers, usuários não-root e healthchecks</strong> transformam seus containers em production-ready, confiáveis e fáceis de manter.</p>

<p>A combinação desses três conceitos — multi-stage + distroless + boas práticas — é o padrão adotado pelas maiores empresas em produção. Aplique-os gradualmente: comece com multi-stage em suas linguagens compiladas, depois migre para distroless em microsserviços críticos, e finalmente implemente healthchecks e scanning em seu pipeline 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: Containerizing apps, not VMs</a></li>

<li><a href="https://docs.docker.com/develop/dev-best-practices/" target="_blank" rel="noopener noreferrer">Dockerfile Best Practices - Docker Docs</a></li>

<li><a href="https://github.com/aquasecurity/trivy" target="_blank" rel="noopener noreferrer">Trivy: Container Image Scanning</a></li>

<li><a href="https://kubernetes.io/docs/tasks/configure-pod-container/security-context/" target="_blank" rel="noopener noreferrer">Kubernetes Security: Running containers as non-root</a></li>

</ul>

<p>&lt;!-- FIM --&gt;</p>

Comentários

Mais em DevOps & CI/CD

O que Todo Dev Deve Saber sobre ArgoCD: GitOps Contínuo para Kubernetes na Prática
O que Todo Dev Deve Saber sobre ArgoCD: GitOps Contínuo para Kubernetes na Prática

O que é ArgoCD e Por que GitOps? ArgoCD é uma ferramenta declarativa de entre...

Feature Flags: como realizar deploys mais seguros e confiáveis
Feature Flags: como realizar deploys mais seguros e confiáveis

A gestão de mudanças no código-fonte de um sistema é uma tarefa constante e d...

Compliance como Código: OPA, Conftest e Policy Enforcement em Kubernetes na Prática
Compliance como Código: OPA, Conftest e Policy Enforcement em Kubernetes na Prática

O Que é Compliance como Código Compliance como Código é um paradigma que tran...