<h2>Entendendo o Problema: Por Que Multi-stage e Distroless?</h2>
<p>Quando você conteineriza uma aplicação Go, há um dilema clássico que todo desenvolvedor enfrenta: o tamanho final da imagem Docker. Uma imagem padrão, construída de forma ingênua, pode chegar facilmente a 1GB ou mais. Isso ocorre porque você precisa de todas as ferramentas de compilação (Go SDK, dependências de build, cache do compilador) apenas para gerar um único binário executável.</p>
<p>A solução é dupla. Primeiro, você usa <strong>multi-stage builds</strong>, que permite compilar seu código em um container com todas as ferramentas necessárias, mas depois copia apenas o binário resultante para um container final. Segundo, você substitui a imagem base tradicional (como Ubuntu ou Debian) por uma imagem <strong>distroless</strong>, que contém apenas o essencial para executar seu programa: o binário, algumas bibliotecas críticas e nada mais. O resultado? Imagens de apenas 20-50MB, mais rápidas de transferir, mais seguras (menos superfície de ataque) e mais baratas de armazenar.</p>
<h2>Dockerfile Multi-stage: Construindo em Camadas</h2>
<h3>O Conceito Fundamental</h3>
<p>Um Dockerfile multi-stage é simplesmente um arquivo Docker com múltiplos comandos <code>FROM</code>. Cada <code>FROM</code> inicia uma nova etapa, um novo "container de trabalho". As etapas anteriores são descartadas, a menos que você copie explicitamente artefatos delas. Isso resolve o problema de bloat porque você não precisa enviar para produção as ferramentas de compilação.</p>
<p>Considere o fluxo: na primeira etapa (stage 1), você usa uma imagem grande contendo Go, git e outras dependências de build. Você baixa as dependências do seu projeto (<code>go mod download</code>), compila seu código (<code>go build</code>) e gera um binário. Na segunda etapa (stage 2), você começa do zero com uma imagem minúscula e copia apenas o binário compilado da etapa anterior.</p>
<h3>Exemplo Prático: Aplicação Go Simples</h3>
<p>Vou mostrar um exemplo real. Suponha que você tenha uma aplicação Go que expõe uma API REST simples:</p>
<pre><code class="language-go">package main
import (
"fmt"
"net/http"
)
func main() {
http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "OK")
})
fmt.Println("Servidor iniciado na porta 8080")
http.ListenAndServe(":8080", nil)
}</code></pre>
<p>Um Dockerfile multi-stage para esta aplicação ficaria assim:</p>
<pre><code class="language-dockerfile"># Stage 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 -a -installsuffix cgo -o main .
Stage 2: Runtime
FROM alpine:latest
WORKDIR /root/
COPY --from=builder /app/main .
EXPOSE 8080
CMD ["./main"]</code></pre>
<p>Analise linha por linha: no stage builder, você herda de <code>golang:1.21-alpine</code> (cerca de 400MB), copia seus arquivos de módulo, baixa dependências e compila. Note <code>CGO_ENABLED=0</code> — isso força Go a compilar estaticamente, sem ligações dinâmicas com a libc do sistema, porque na segunda etapa você pode não ter as mesmas bibliotecas.</p>
<p>No stage final, você herda de <code>alpine:latest</code> (apenas 7MB) e copia o binário já compilado com <code>COPY --from=builder</code>. Pronto. Sua imagem final terá por volta de 15-20MB.</p>
<h3>Por Que <code>CGO_ENABLED=0</code> Importa</h3>
<p>Go pode ser compilado de duas formas: estaticamente (CGO_ENABLED=0) ou dinamicamente (CGO_ENABLED=1, o padrão). Se você compilar com CGO ativado e depois tentar rodar em uma imagem distroless ou alpine que não tenha a libc esperada, seu programa falhará com erros de biblioteca não encontrada. Sempre use <code>CGO_ENABLED=0</code> em builds multi-stage, a menos que você realmente saiba que precisa de bibliotecas C dinâmicas.</p>
<h2>Imagens Distroless: Segurança e Leveza Extrema</h2>
<h3>O Que São Imagens Distroless?</h3>
<p>Imagens distroless são construídas pelo Google especificamente para rodar linguagens compiladas. Diferente de Alpine (que ainda é um Linux completo, com shell, apt, etc.), uma imagem distroless contém apenas: um glibc minimalista, bibliotecas de tempo de execução essenciais e nada mais. Sem shell, sem gerenciador de pacotes, sem utilitários. Você não consegue nem fazer <code>docker run -it distroless /bin/sh</code> porque não há shell.</p>
<p>Isso traz dois benefícios imensuráveis: <strong>segurança</strong> (CVEs não-exploráveis sem shell ou ferramentas de ataque) e <strong>tamanho</strong> (geralmente menores que Alpine). Para aplicações Go pura (sem CGO), a imagem distroless é praticamente perfeita.</p>
<h3>Exemplo Prático: Multi-stage com Distroless</h3>
<p>Aqui está a mesma aplicação, mas usando distroless:</p>
<pre><code class="language-dockerfile"># Stage 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 -a -installsuffix cgo -o main .
Stage 2: Runtime com Distroless
FROM gcr.io/distroless/base-debian11
WORKDIR /app
COPY --from=builder /app/main .
EXPOSE 8080
ENTRYPOINT ["/app/main"]</code></pre>
<p>A diferença é sutil mas poderosa: ao invés de <code>FROM alpine:latest</code>, você usa <code>FROM gcr.io/distroless/base-debian11</code>. Essa imagem tem aproximadamente 19MB (comparada aos 7MB de Alpine, mas com muito menos "gordura" real). O binário Go rodará perfeitamente porque Go está linkado estaticamente.</p>
<h3>Escolhendo a Imagem Distroless Correta</h3>
<p>Google oferece várias variantes de distroless. As principais são:</p>
<ul>
<li><strong><code>gcr.io/distroless/base-debian11</code></strong>: Contém libc e outras bibliotecas padrão. Use quando seu programa depende de bibliotecas do sistema.</li>
<li><strong><code>gcr.io/distroless/static-debian11</code></strong>: Ainda mais minimalista. Use apenas se seu programa for 100% static-linked (Go puro).</li>
<li><strong><code>gcr.io/distroless/cc-debian11</code></strong>: Inclui suporte a C++ e C. Use se tiver CGO ou dependências C.</li>
</ul>
<p>Para Go puro, <code>static-debian11</code> é o ideal:</p>
<pre><code class="language-dockerfile"># Stage 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 -a -installsuffix cgo -o main .
Stage 2: Runtime com Distroless Static
FROM gcr.io/distroless/static-debian11
COPY --from=builder /app/main /app/main
EXPOSE 8080
ENTRYPOINT ["/app/main"]</code></pre>
<p>Essa imagem terá menos de 10MB de overhead, porque <code>static-debian11</code> é apenas um scratch (imagem vazia) com umas camadas mínimas de metadados.</p>
<h2>Otimizações Avançadas e Boas Práticas</h2>
<h3>Cacheando Dependências Correctamente</h3>
<p>Go usa módulos e o comando <code>go mod download</code> pode ser custoso (especialmente com muitas dependências). Um erro comum é copiar todo o código primeiro e depois fazer download das dependências. Melhor prática: copie <code>go.mod</code> e <code>go.sum</code> <em>antes</em> de copiar o código-fonte. Assim, se o código mudar mas as dependências não, Docker reutiliza aquela camada do cache.</p>
<pre><code class="language-dockerfile">FROM golang:1.21-alpine AS builder
WORKDIR /app
Copiar apenas os arquivos de módulo
COPY go.mod go.sum ./
RUN go mod download
Depois copiar o código (muda frequentemente)
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main .</code></pre>
<h3>Build Args para Versionamento</h3>
<p>É bom incluir informações de build (versão, commit hash) no seu binário. Use build args:</p>
<pre><code class="language-dockerfile">FROM golang:1.21-alpine AS builder
ARG VERSION=dev
ARG COMMIT=unknown
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build \
-ldflags="-X main.Version=${VERSION} -X main.Commit=${COMMIT}" \
-a -installsuffix cgo -o main .
FROM gcr.io/distroless/static-debian11
COPY --from=builder /app/main /app/main
EXPOSE 8080
ENTRYPOINT ["/app/main"]</code></pre>
<p>No seu código Go:</p>
<pre><code class="language-go">package main
var (
Version = "dev"
Commit = "unknown"
)
func main() {
fmt.Printf("Versão: %s, Commit: %s\n", Version, Commit)
// ... resto do código
}</code></pre>
<p>Para buildar: <code>docker build --build-arg VERSION=1.0.0 --build-arg COMMIT=abc123def .</code></p>
<h3>Reduzindo Ainda Mais com <code>.dockerignore</code></h3>
<p>Certifique-se de que você não está copiando arquivos desnecessários. Crie um arquivo <code>.dockerignore</code>:</p>
<pre><code>.git
.gitignore
README.md
.env
vendor
test
*.test</code></pre>
<p>Isso evita que você copie gigabytes de histórico git ou diretórios de teste para a camada de build.</p>
<h3>Exemplo Real: Aplicação Completa com Go Modules</h3>
<p>Para um projeto mais realista com dependências externas:</p>
<pre><code class="language-dockerfile"># Stage 1: Build
FROM golang:1.21-alpine AS builder
WORKDIR /app
Cache de dependências
COPY go.mod go.sum ./
RUN go mod download
Cópia do código
COPY . .
Build com flags otimizadas
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \
-ldflags="-s -w" \
-o main .
Stage 2: Runtime Distroless
FROM gcr.io/distroless/static-debian11
COPY --from=builder /app/main /app/main
EXPOSE 8080
ENTRYPOINT ["/app/main"]</code></pre>
<p>Note a flag <code>-ldflags="-s -w"</code>: ela remove symbols e debug info do binário final, reduzindo seu tamanho em até 30%. Use isso em produção, mas não em desenvolvimento (perderá a capacidade de debugar).</p>
<h2>Conclusão</h2>
<p>Você aprendeu que <strong>containers Docker para Go não precisam ser pesados</strong>. Usando multi-stage builds, você separa o environment de compilação do de execução, eliminando gigabytes de ferramentas desnecessárias. Combinado com imagens distroless, você chega a imagens finais menores que 20MB sem sacrificar funcionalidade ou segurança.</p>
<p>A segunda lição é que <strong>cada detalhe importa</strong>: <code>CGO_ENABLED=0</code> garante que você não depende de bibliotecas dinâmicas, <code>.dockerignore</code> impede cópias desnecessárias, e o cache correto de módulos economiza tempo de rebuild. Por fim, <strong>distroless não é apenas um luxo</strong>, é uma prática recomendada de segurança em produção. Menos código desnecessário significa menos superfície de ataque e menos atualizações de segurança para gerenciar.</p>
<h2>Referências</h2>
<ul>
<li><a href="https://github.com/GoogleContainerTools/distroless" target="_blank" rel="noopener noreferrer">Google Distroless Documentation</a></li>
<li><a href="https://docs.docker.com/build/building/multi-stage/" target="_blank" rel="noopener noreferrer">Docker Multi-stage Builds</a></li>
<li><a href="https://golang.org/cmd/go/#hdr-Build_constraints" target="_blank" rel="noopener noreferrer">Go Build Constraints and Build Tags</a></li>
<li><a href="https://www.docker.com/blog/containerized-golang-applications/" target="_blank" rel="noopener noreferrer">Building Docker Images for Go Applications</a></li>
<li><a href="https://www.digitalocean.com/community/tutorials/how-to-build-go-executables-for-multiple-platforms-on-ubuntu-and-windows" target="_blank" rel="noopener noreferrer">CGO and Static Linking in Go</a></li>
</ul>
<p><!-- FIM --></p>