O Que São Imagens Distroless?
Imagens distroless são contêineres Docker minimalistas que contêm apenas o necessário para executar sua aplicação: o binário compilado e suas dependências de runtime. Elas não incluem gerenciadores de pacotes, shells, utilitários Unix tradicionais ou qualquer outro software que não seja estritamente necessário para rodar o programa.
O conceito foi desenvolvido pelo Google e é mantido no repositório distroless. A ideia é radical: se sua aplicação é um binário Go compilado, por que incluir bash, apt, systemd e outras centenas de ferramentas que jamais serão usadas? Essa filosofia reduz drasticamente o tamanho da imagem e, mais importante, elimina superfícies de ataque. Se não existe shell no contêiner, um atacante não pode executar comandos interativos mesmo que encontre uma vulnerabilidade.
Entendendo Alpine Linux
Alpine Linux é uma distribuição baseada em musl e busybox que oferece um ponto intermediário entre distroless e distribuições completas como Ubuntu ou Debian. Ela inclui um gerenciador de pacotes (apk), um shell mínimo e ferramentas básicas, mantendo um tamanho reduzido (geralmente 5-10MB base).
Alpine é útil quando você precisa instalar dependências em tempo de build ou quando sua aplicação requer algumas ferramentas do sistema. É mais prática que distroless para processos de compilação complexos, mas oferece compromisso entre segurança e funcionalidade. Ao contrário de distroless, você pode fazer debug acessando o contêiner com docker exec, embora isso vá contra as melhores práticas de produção.
Por Que Alpine é Menor que Ubuntu?
Uma imagem Ubuntu base tem ~77MB, enquanto Alpine tem ~7MB. Isso ocorre porque Alpine não inclui a glibc (GNU C Library) pesada, usando musl em seu lugar. O musl é uma implementação de libc mais compacta, e busybox agrupa múltiplos utilitários Unix em um único binário. Você perde compatibilidade com algumas bibliotecas legadas compiladas para glibc, mas ganha tamanho significativamente menor.
Comparação Prática: Distroless vs Alpine vs Tradicional
Vamos construir a mesma aplicação Go em três cenários diferentes e comparar tamanhos e segurança.
Exemplo 1: Dockerfile com Ubuntu (Baseline)
FROM ubuntu:22.04
RUN apt-get update && apt-get install -y \
golang-go \
git \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY main.go .
RUN go build -o app main.go
CMD ["./app"]
Tamanho final: ~500MB (Ubuntu base + Go toolchain)
Exemplo 2: Dockerfile Multistage com Alpine
# Stage 1: Builder
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY main.go .
RUN go build -o app main.go
# Stage 2: Runtime
FROM alpine:3.18
RUN apk add --no-cache ca-certificates
WORKDIR /app
COPY --from=builder /app/app .
CMD ["./app"]
Tamanho final: ~15-20MB (dependendo das dependências)
Exemplo 3: Dockerfile com Distroless
# Stage 1: Builder
FROM golang:1.21 AS builder
WORKDIR /app
COPY main.go .
RUN go build -o app main.go
# Stage 2: Runtime com distroless
FROM gcr.io/distroless/base-debian12
COPY --from=builder /app/app /app
ENTRYPOINT ["/app"]
Tamanho final: ~20-30MB (distroless base + binário)
Aplicação Go Exemplo Completa
Para testar os Dockerfiles acima, use esta aplicação simples:
// main.go
package main
import (
"fmt"
"net/http"
"os"
)
func handler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello from distroless/alpine!\n")
}
func main() {
http.HandleFunc("/", handler)
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}
fmt.Printf("Server running on port %s\n", port)
http.ListenAndServe(":"+port, nil)
}
Compile localmente:
go build -o app main.go
Para a versão distroless, o binário deve ser compilado de forma estática ou com as dependências necessárias incluídas. A imagem distroless não oferece ferramentas de debug tradicional, então você precisará estruturar logs e monitoramento corretamente na sua aplicação.
Segurança em Produção
Redução de Superfície de Ataque
Imagens distroless eliminam a maioria dos vetores de ataque comuns. Sem shell (/bin/sh), um atacante não pode executar comandos mesmo comprometendo a aplicação. Sem apt, yum ou apk, não é possível instalar backdoors pós-exploração. Uma análise de segurança em uma imagem distroless típica mostra zero vulnerabilidades conhecidas em componentes do SO porque praticamente não há componentes.
Compare com Alpine, que ainda inclui apk: um atacante poderia teoricamente usar apk add para instalar ferramentas maliciosas. Não é um risco trivial, mas existe. Ubuntu e Debian têm centenas de pacotes pré-instalados, multiplicando exponencialmente os riscos.
Análise de Vulnerabilidades com Trivy
Aqui está como avaliar imagens reais:
# Instalar trivy (se não tiver)
# https://github.com/aquasecurity/trivy
# Escanear imagem Ubuntu
trivy image ubuntu:22.04
# Escanear imagem Alpine
trivy image alpine:3.18
# Escanear imagem Distroless
trivy image gcr.io/distroless/base-debian12
Resultados típicos:
- Ubuntu 22.04: 100+ vulnerabilidades (muitas críticas)
- Alpine 3.18: 5-10 vulnerabilidades (geralmente baixas)
- Distroless: 0-2 vulnerabilidades (patches mínimos)
Best Practices de Segurança
# Use distroless quando possível
FROM gcr.io/distroless/base-debian12:nonroot
# Se distroless não for viável, prefira alpine a ubuntu
FROM alpine:3.18
# Sempre faça builds multistage para remover ferramentas de compilação
FROM golang:1.21-alpine AS builder
RUN go build -ldflags="-w -s" -o app main.go
FROM alpine:3.18
COPY --from=builder /app/app /app
# Nunca rode como root
RUN addgroup -g 1000 appuser && \
adduser -D -u 1000 -G appuser appuser
USER appuser
CMD ["/app"]
Quando Usar Cada Uma
Use Distroless Quando:
- Sua aplicação é um binário compilado (Go, Rust, C compilado estaticamente)
- Você não precisa de ferramentas de debug em produção (logs estruturados são suficientes)
- Segurança máxima é crítico (aplicações financeiras, dados sensíveis)
- Você quer imagens o mais pequenas possível (edge computing, serverless)
Use Alpine Quando:
- Você precisa executar comandos em tempo de build (instalar dependências específicas)
- Sua linguagem é interpretada (Python, Node.js) e requer binários adicionais
- Você ocasionalmente precisa debugar em produção via
docker exec - A compatibilidade com
glibcé necessária (alguns binários legados)
Use Ubuntu/Debian Quando:
- Sua aplicação tem dependências complexas que requerem o ecossistema completo
- Você está em ambiente legado onde mudanças arriscadas devem ser evitadas
- A diferença de 100-500MB não impacta seus custos ou infraestrutura
Exemplo Real: Aplicação Python com Alpine
Python é um caso mais realista onde Alpine é preferível a distroless, pois a interpretação requer a runtime Python compilada:
# Stage 1: Builder
FROM python:3.11-alpine AS builder
WORKDIR /app
COPY requirements.txt .
# Instalar apenas o essencial de build
RUN apk add --no-cache --virtual .build-deps \
gcc \
musl-dev \
&& pip install --no-cache-dir --user -r requirements.txt \
&& apk del .build-deps
# Stage 2: Runtime
FROM python:3.11-alpine
# Instalar apenas runtime necessário
RUN apk add --no-cache \
ca-certificates \
libffi-dev
WORKDIR /app
COPY --from=builder /root/.local /root/.local
COPY app.py .
ENV PATH=/root/.local/bin:$PATH
RUN addgroup -g 1000 appuser && \
adduser -D -u 1000 -G appuser appuser
USER appuser
CMD ["python", "app.py"]
Aplicação Python correspondente:
# app.py
from flask import Flask
import os
app = Flask(__name__)
@app.route('/')
def hello():
return 'Hello from Alpine!\n'
if __name__ == '__main__':
port = os.getenv('PORT', 8080)
app.run(host='0.0.0.0', port=int(port))
Este Dockerfile com Alpine resultará em ~200-250MB, muito menos que ~800MB com Debian.
Otimizações Adicionais
Compilação Estática em Go
Para distroless ser verdadeiramente eficaz, compile Go sem dependências dinâmicas:
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -installsuffix cgo -ldflags="-w -s" -o app main.go
As flags -w -s removem símbolos de debug, reduzindo tamanho do binário em 30-40%.
Verificação de Tamanhos Localmente
# Build das três imagens
docker build -f Dockerfile.ubuntu -t app:ubuntu .
docker build -f Dockerfile.alpine -t app:alpine .
docker build -f Dockerfile.distroless -t app:distroless .
# Comparar tamanhos
docker images | grep app
# Output típico:
# app ubuntu 500MB
# app alpine 18MB
# app distroless 25MB
Conclusão
Imagens distroless e Alpine representam uma evolução crítica em como construímos aplicações containerizadas para produção. A escolha entre elas não é teórica—é uma decisão entre segurança máxima (distroless) e praticidade com tamanho reduzido (Alpine). Ubuntu e Debian continuam tendo lugar em cenários legados, mas devem ser a exceção, não a regra.
Três aprendizados principais para levar adiante: Primeiro, sempre use builds multistage, independentemente da imagem base—separar ferramentas de compilação de runtime é tão importante quanto a escolha da base. Segundo, teste suas imagens com scanners de vulnerabilidade (Trivy, Snyk) antes de promover para produção; os números não mentem. Terceiro, entenda que distroless não é "melhor" que Alpine universalmente—use a ferramenta certa para o trabalho, mas comece com distroless e considere Alpine apenas quando você tiver razões técnicas legítimas para isso.