Como reduzir drasticamente o tamanho de uma imagem Docker para aplicações Go

Como reduzir drasticamente o tamanho de uma imagem Docker para aplicações Go

Uma das características mais úteis do Go para quem trabalha com containers costuma passar despercebida: a linguagem compila para um binário estático, sem depender de um runtime instalado no sistema. Isso abre espaço para uma técnica simples que muda completamente o resultado final de uma imagem Docker, o multi-stage build. Neste artigo mostro como aplicá-la em uma aplicação Go, por que ela funciona e quais decisões entram no caminho até chegar em uma imagem de produção enxuta e segura.


O problema de partida

Um Dockerfile básico para uma aplicação Go costuma seguir esta estrutura: parte da imagem golang, copia o go.mod, baixa as dependências, copia o código fonte e compila o binário.

FROM golang:1.24
WORKDIR /app
COPY go.mod ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o /app-binario ./cmd/main.go
CMD ["/app-binario"]

Esse Dockerfile funciona e é um bom ponto de partida para entender os comandos básicos do Docker. O detalhe é que a imagem final carrega dentro dela tudo que a imagem golang traz por padrão: o compilador, o cache de módulos, ferramentas de linha de comando e uma distribuição Linux completa por baixo. Isso empurra o tamanho da imagem para a casa de 800MB, mesmo quando o binário compilado tem poucos megabytes. Em produção, nada disso é necessário. A aplicação só precisa do executável pronto.


A técnica: multi-stage build

O multi-stage build permite declarar mais de uma imagem base dentro do mesmo Dockerfile, cada uma responsável por uma etapa diferente. A ideia central é separar quem compila de quem executa. Um estágio usa a imagem completa do Go só para gerar o binário, e um segundo estágio parte de uma imagem mínima, copiando apenas o executável pronto do primeiro estágio.

# Estágio de build
FROM golang:1.24 AS builder
WORKDIR /app
COPY go.mod ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /app-binario ./cmd/main.go


# Estágio final
FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=builder /app-binario /app-binario
USER nonroot:nonroot
ENTRYPOINT ["/app-binario"]

Dois detalhes fazem essa técnica funcionar bem especificamente para Go. O primeiro é CGO_ENABLED=0, que desativa a dependência de bibliotecas C do sistema operacional e gera um binário estaticamente vinculado, capaz de rodar sozinho em uma imagem praticamente vazia. O segundo é a flag -ldflags="-s -w", que remove símbolos de debug e tabelas do binário, reduzindo ainda mais o seu tamanho final.

O resultado prático costuma ser expressivo. Uma imagem que passava de 800MB cai para algo entre 10MB e 20MB, uma redução na casa de 98%. Isso se traduz em deploys mais rápidos, menos custo de armazenamento em registries de imagem e um tempo de download bem menor quando um novo container precisa subir, o que importa bastante em ambientes que escalam automaticamente.


Escolhendo a imagem do estágio final

Depois que o binário está pronto, ainda existe uma decisão importante sobre qual imagem usar no segundo estágio. As três opções mais comuns têm trade-offs diferentes.

A imagem scratch é literalmente vazia, sem sistema operacional nenhum. É a menor opção possível, mas exige copiar manualmente qualquer coisa que o binário precise, como os certificados de autoridade certificadora para chamadas HTTPS, e não oferece shell nenhum para depuração dentro do container.

As imagens distroless, mantidas pelo Google, ocupam um meio termo. Trazem os certificados e bibliotecas mínimas do sistema, mas continuam sem shell, sem gerenciador de pacotes e sem qualquer ferramenta que não seja estritamente necessária para rodar o binário. Por não terem essas ferramentas, reduzem bastante a superfície de ataque da imagem. A variante nonroot, usada no exemplo acima, já vem configurada para rodar a aplicação sem privilégios de root por padrão.

O Alpine é a opção mais flexível, porque tem um shell e um gerenciador de pacotes leve, o que ajuda quando é preciso entrar no container para investigar um problema. Em compensação, ele usa a biblioteca musl no lugar da glibc, o que ocasionalmente causa incompatibilidades sutis com binários Go compilados sem cuidado.

Para times que não têm necessidade de depurar containers em produção com frequência, distroless costuma ser o equilíbrio mais sensato entre segurança e praticidade. Para quem quer poder abrir um shell dentro do container quando algo dá errado, Alpine é a escolha mais confortável.


Rodando como usuário não root

Por padrão, um container Docker roda como root, o que quer dizer que qualquer vulnerabilidade explorada dentro da aplicação herda privilégios de root dentro do container. Por isso, um dos ajustes mais importantes é adicionar a instrução USER no Dockerfile, apontando para um usuário sem privilégios.

Quando se usa uma imagem distroless na variante nonroot, esse cuidado já vem configurado por padrão, como no exemplo anterior. Em outras imagens, como Alpine, é preciso criar o usuário manualmente antes de trocar para ele:

RUN addgroup -g 1001 appgroup && adduser -D -u 1001 -G appgroup appuser
USER appuser

Um detalhe que vale reforçar: qualquer comando que precise escrever arquivos ou instalar dependências deve rodar antes da instrução USER. Depois que o Dockerfile troca de usuário, todos os comandos seguintes, incluindo o CMD ou ENTRYPOINT, rodam com as permissões desse usuário limitado.


Mantendo a agilidade no desenvolvimento

Compilar e reiniciar manualmente a cada alteração de código dentro de um container mata a produtividade rapidamente, então vale separar o Dockerfile de desenvolvimento do Dockerfile de produção que construímos acima.

Uma opção é o Air, uma ferramenta que observa o código fonte e recompila e reinicia a aplicação automaticamente a cada alteração salva. No ambiente de desenvolvimento, o Dockerfile instala o Air e monta o código como volume, em vez de copiá-lo de forma fixa na imagem, permitindo que as alterações feitas no editor apareçam imediatamente dentro do container.

Outra opção é o modo Watch do Docker Compose, disponível a partir da versão 2.22. Ele permite declarar, dentro do docker-compose.yml, quais diretórios devem ser sincronizados automaticamente para dentro do container quando um arquivo muda, com a opção de apenas copiar o arquivo ou também reiniciar o serviço. Para aplicações Go, a combinação mais comum é sincronizar o código e reiniciar o processo, já que a linguagem precisa ser recompilada a cada mudança.

Nenhuma dessas ferramentas deve ir para a imagem de produção. A prática mais comum é manter um Dockerfile de desenvolvimento com essas facilidades de recarregamento automático, e o Dockerfile enxuto com multi-stage build, mostrado no início deste artigo, para a imagem que efetivamente vai para produção.


Fechando a ideia

Aproveitar a compilação estática do Go através de multi-stage builds é uma das técnicas com melhor custo-benefício para quem já usa Docker no dia a dia. Ela não exige mudanças na aplicação, só uma reorganização de como a imagem é construída, e o ganho em tamanho, velocidade de deploy e segurança é imediato. Combinada com a escolha certa de imagem base e com o cuidado de rodar como usuário não root, essa técnica é o que separa um Dockerfile que só funciona de um Dockerfile pronto para produção.


Fontes



Comentários (0)

Nenhum comentário ainda. Seja o primeiro.

Continue lendo

Entendendo o Conceito de Estado no React

Entendendo o Conceito de Estado no React

Em React, o estado refere-se a uma estrutura de dados que determina o comportamento do componente e como ele será renderizado na interface. Por exemplo, o estado de um componente pode incluir dados como o valor atual de…

Artigo, FrontEnd, Iniciantes, Intermediários, React
Como rodar Laravel no WSL com Valet Linux (de verdade)

Como rodar Laravel no WSL com Valet Linux (de verdade)

Sim, dá pra desenvolver Laravel no Windows sem sofrer. Mas você precisa fazer do jeito certo: usando WSL + Valet Linux. Esqueça XAMPP, Laragon, Docker lento e até o WSL 1. O caminho mais fluido para trabalhar com…

Laravel, NGINX, php, Tutorial