Empacotando Go com Docker
LidoNesta aula, você aprenderá a empacotar aplicações Go com Docker, usando multi-stage builds, imagens mínimas e binários estáticos. Vamos explorar boas práticas para criar imagens pequenas, seguras e eficientes, com exemplos práticos de Dockerfile.
Empacotar aplicações Go com Docker é uma habilidade essencial para desenvolvimento e deploy em ambientes modernos. Nesta aula, vamos explorar como criar imagens Docker eficientes para aplicações Go, aproveitando as características únicas da linguagem, como a compilação estática e a ausência de dependências em tempo de execução. Você aprenderá a usar multi-stage builds, imagens mínimas e boas práticas para produzir imagens pequenas, seguras e fáceis de manter.
O Docker permite isolar sua aplicação em um ambiente consistente, mas sem as técnicas certas, as imagens podem ficar grandes e vulneráveis. Com Go, temos a vantagem de gerar binários estáticos que não dependem de bibliotecas externas, o que nos permite usar imagens mínimas como scratch ou alpine. Vamos detalhar cada etapa do processo, desde a compilação até a execução final.
Multi-stage build
O multi-stage build é um recurso do Docker que permite usar múltiplas imagens em um mesmo Dockerfile, copiando apenas o que é necessário para a fase final. Isso é extremamente útil para Go, pois podemos usar uma imagem grande com todas as ferramentas de compilação na primeira etapa, e depois copiar apenas o binário compilado para uma imagem final enxuta.
Na primeira etapa, usamos a imagem oficial do Go (golang:1.22 ou similar) que contém o compilador e todas as ferramentas necessárias. Compilamos o código e geramos o binário. Na segunda etapa, copiamos esse binário para uma imagem mínima, como scratch ou alpine, e configuramos o comando de execução. Isso reduz drasticamente o tamanho da imagem final, pois não inclui o compilador, bibliotecas de desenvolvimento ou outros arquivos desnecessários.
Vamos ver um exemplo básico de Dockerfile com multi-stage build:
# Etapa 1: compilação
FROM golang:1.22 AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN go build -o /app/meuapp cmd/main.go
# Etapa 2: imagem final
FROM scratch
COPY --from=builder /app/meuapp /meuapp
ENTRYPOINT ["/meuapp"]
Neste exemplo, a primeira etapa usa a imagem golang:1.22 para compilar o código. A segunda etapa usa scratch, que é uma imagem vazia, e copia o binário compilado. O resultado é uma imagem extremamente pequena, contendo apenas o executável e nenhum sistema operacional ou bibliotecas.
Se sua aplicação precisar de certificados SSL ou de arquivos de configuração, você pode copiá-los para a imagem final. Por exemplo, para SSL, você pode copiar os certificados CA da imagem alpine:
FROM alpine:latest AS certs
RUN apk --no-cache add ca-certificates
FROM scratch
COPY --from=certs /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /app/meuapp /meuapp
ENTRYPOINT ["/meuapp"]
Imagens mínimas
Imagens mínimas são aquelas que contêm apenas o essencial para executar sua aplicação. Para Go, as opções mais comuns são scratch e alpine. A imagem scratch é literalmente vazia, não possui shell, bibliotecas ou utilitários. Já alpine é uma distribuição Linux mínima, com cerca de 5 MB, que inclui o gerenciador de pacotes apk e alguns utilitários básicos.
Usar scratch é ideal quando seu binário Go é totalmente estático e não precisa de nenhum recurso do sistema. No entanto, se você precisar de resolução de DNS (para fazer requisições HTTP a domínios, por exemplo), é necessário incluir os certificados CA e o arquivo /etc/passwd para o usuário não-root. A imagem alpine já vem com esses arquivos, facilitando a configuração.
Aqui está um exemplo usando alpine como imagem final:
FROM golang:1.22 AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN go build -o /app/meuapp cmd/main.go
FROM alpine:latest
RUN apk --no-cache add ca-certificates
COPY --from=builder /app/meuapp /usr/local/bin/meuapp
ENTRYPOINT ["meuapp"]
A imagem alpine é útil quando você precisa de um shell para depuração ou de utilitários como curl para health checks. No entanto, lembre-se de que ela adiciona alguns megabytes à imagem final, o que pode ser aceitável dependendo do contexto.
Binário estático
Go compila para binários estáticos por padrão, o que significa que o executável não depende de bibliotecas dinâmicas do sistema, como glibc. Isso é uma grande vantagem para Docker, pois podemos copiar o binário para qualquer imagem, mesmo scratch, e ele funcionará sem problemas.
No entanto, em alguns casos, especialmente ao usar o pacote net ou os/user, o Go pode usar a biblioteca cgo para acessar funcionalidades do sistema, resultando em binários dinamicamente vinculados. Para garantir um binário totalmente estático, compile com a variável de ambiente CGO_ENABLED=0. Isso desativa o cgo e usa implementações puras em Go, como o resolvedor DNS nativo.
Veja como compilar um binário estático:
CGO_ENABLED=0 go build -o meuapp cmd/main.go
No Dockerfile, você pode definir essa variável na etapa de build:
FROM golang:1.22 AS builder
ENV CGO_ENABLED=0
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN go build -o /app/meuapp cmd/main.go
Com CGO_ENABLED=0, o binário será estático e poderá ser executado em imagens como scratch sem problemas. Teste localmente com file meuapp para verificar se está estático: a saída deve indicar statically linked.
Outra vantagem é que binários estáticos são mais fáceis de transferir e executar em qualquer ambiente, sem se preocupar com dependências do sistema. Isso torna o Docker o veículo perfeito para distribuir aplicações Go.
Boas práticas
Ao empacotar Go com Docker, algumas boas práticas ajudam a melhorar a segurança, o desempenho e a manutenção das imagens. Primeiro, sempre use uma imagem base oficial e específica, como golang:1.22 em vez de golang:latest, para garantir consistência e evitar quebras por mudanças inesperadas.
Segundo, aproveite o cache do Docker. No Dockerfile, copie os arquivos go.mod e go.sum antes do código-fonte, e execute go mod download em uma camada separada. Assim, o Docker pode reutilizar essa camada quando as dependências não mudarem, acelerando builds subsequentes.
Terceiro, execute a aplicação como um usuário não-root na imagem final. Isso aumenta a segurança, pois limita o impacto de uma possível vulnerabilidade. Em scratch, você precisa criar um usuário e grupo, ou usar alpine e o comando adduser. Veja um exemplo com alpine:
FROM alpine:latest
RUN apk --no-cache add ca-certificates && adduser -D appuser
USER appuser
COPY --from=builder /app/meuapp /usr/local/bin/meuapp
ENTRYPOINT ["meuapp"]
Quarto, minimize o número de camadas. Combine comandos relacionados em um único RUN quando possível, e use --from para copiar apenas o necessário. Isso reduz o tamanho da imagem e melhora a performance.
Quinto, use o comando docker scan ou docker scout para analisar a imagem em busca de vulnerabilidades. Imagens mínimas como scratch não têm pacotes para escanear, mas alpine pode ter vulnerabilidades em suas bibliotecas. Manter a imagem atualizada é fundamental.
Por fim, considere usar o .dockerignore para excluir arquivos desnecessários do contexto de build, como .git, node_modules (se houver) e arquivos temporários. Isso acelera o build e evita que segredos sejam copiados para a imagem.
Referências
- Docker Docs: Multi-stage builds
- Docker Docs: Create a base image
- Go Documentation: Installing Go
- Go Wiki: GoArm
- Dockerfile reference
- Docker Scan
- Google Distroless Images
Exercícios
- Crie um Dockerfile multi-stage para uma aplicação Go simples que imprime "Hello, Docker!" na saída padrão. Use
golang:1.22na primeira etapa escratchna segunda. Certifique-se de que o binário seja estático.✓ Resposta:FROM golang:1.22 AS builder ENV CGO_ENABLED=0 WORKDIR /app COPY go.mod go.sum ./ RUN go mod download COPY . . RUN go build -o /app/hello . FROM scratch COPY --from=builder /app/hello /hello ENTRYPOINT ["/hello"] - Explique a diferença entre usar
scratchealpinecomo imagem final para uma aplicação Go. Em que situações você escolheria uma ou outra?✓ Resposta:scratché uma imagem vazia, ideal para binários totalmente estáticos que não precisam de recursos do sistema. É a menor imagem possível, mas não possui shell, certificados CA ou utilitários.alpineé uma distribuição Linux mínima (~5 MB) que inclui um shell, gerenciador de pacotes e certificados CA, facilitando a configuração de rede e a adição de ferramentas. Escolhascratchpara máxima minimalismo e segurança, mas se precisar de suporte a DNS, certificados SSL ou ferramentas de debug, usealpine. - Escreva um Dockerfile que compile uma aplicação Go com
CGO_ENABLED=0e copie os certificados CA para a imagem finalscratch. Use a imagemalpinecomo fonte dos certificados.✓ Resposta:FROM golang:1.22 AS builder ENV CGO_ENABLED=0 WORKDIR /app COPY go.mod go.sum ./ RUN go mod download COPY . . RUN go build -o /app/meuapp . FROM alpine:latest AS certs RUN apk --no-cache add ca-certificates FROM scratch COPY --from=certs /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ COPY --from=builder /app/meuapp /meuapp ENTRYPOINT ["/meuapp"] - Descreva três boas práticas para otimizar o build de uma imagem Docker para Go. Explique como cada uma melhora o processo.✓ Resposta: 1. **Copiar go.mod e go.sum antes do código-fonte**: Isso permite que o Docker use o cache das camadas de dependências. Se as dependências não mudarem, o `go mod download` não será reexecutado, acelerando builds. 2. **Usar multi-stage build**: Separa a compilação da imagem final, reduzindo o tamanho da imagem ao não incluir ferramentas de desenvolvimento. 3. **Executar como usuário não-root**: Aumenta a segurança, limitando privilégios dentro do contêiner. 4. **Usar .dockerignore**: Exclui arquivos desnecessários do contexto de build, acelerando o envio e evitando que segredos sejam copiados.
- Modifique o Dockerfile do exercício 1 para executar a aplicação como um usuário não-root. Use
alpinecomo imagem final e crie um usuário chamadoappuser.✓ Resposta:FROM golang:1.22 AS builder ENV CGO_ENABLED=0 WORKDIR /app COPY go.mod go.sum ./ RUN go mod download COPY . . RUN go build -o /app/hello . FROM alpine:latest RUN apk --no-cache add ca-certificates && adduser -D appuser USER appuser COPY --from=builder /app/hello /hello ENTRYPOINT ["/hello"]
Boas práticas e observações finais
Nesta aula, você aprendeu a empacotar aplicações Go com Docker de forma eficiente. Lembre-se de sempre testar suas imagens localmente e usar ferramentas como docker build e docker run para validar. A combinação de multi-stage builds, imagens mínimas e binários estáticos resulta em imagens pequenas e seguras, ideais para produção.
Além disso, considere automatizar o processo de build com CI/CD e usar registries privados para armazenar suas imagens. O Docker é uma ferramenta poderosa, e com Go você pode aproveitar ao máximo seus recursos para criar deploys rápidos e confiáveis.