Docker & Kubernetes

Dominando Imagens Distroless e Alpine: Segurança e Tamanho em Produção em Projetos Reais

12 min de leitura

Dominando Imagens Distroless e Alpine: Segurança e Tamanho em Produção em Projetos Reais

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 . 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 e que oferece um ponto intermediário entre distroless e distribuições completas como Ubuntu ou Debian. Ela inclui um gerenciador de pacotes ( ), um shell mínimo e

<h2>O Que São Imagens Distroless?</h2>

<p>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.</p>

<p>O conceito foi desenvolvido pelo Google e é mantido no repositório <code>distroless</code>. 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.</p>

<h2>Entendendo Alpine Linux</h2>

<p>Alpine Linux é uma distribuição baseada em <code>musl</code> e <code>busybox</code> que oferece um ponto intermediário entre distroless e distribuições completas como Ubuntu ou Debian. Ela inclui um gerenciador de pacotes (<code>apk</code>), um shell mínimo e ferramentas básicas, mantendo um tamanho reduzido (geralmente 5-10MB base).</p>

<p>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ê <em>pode</em> fazer debug acessando o contêiner com <code>docker exec</code>, embora isso vá contra as melhores práticas de produção.</p>

<h3>Por Que Alpine é Menor que Ubuntu?</h3>

<p>Uma imagem Ubuntu base tem ~77MB, enquanto Alpine tem ~7MB. Isso ocorre porque Alpine não inclui a <code>glibc</code> (GNU C Library) pesada, usando <code>musl</code> em seu lugar. O <code>musl</code> é uma implementação de libc mais compacta, e <code>busybox</code> agrupa múltiplos utilitários Unix em um único binário. Você perde compatibilidade com algumas bibliotecas legadas compiladas para <code>glibc</code>, mas ganha tamanho significativamente menor.</p>

<h2>Comparação Prática: Distroless vs Alpine vs Tradicional</h2>

<p>Vamos construir a mesma aplicação Go em três cenários diferentes e comparar tamanhos e segurança.</p>

<h3>Exemplo 1: Dockerfile com Ubuntu (Baseline)</h3>

<pre><code class="language-dockerfile">FROM ubuntu:22.04

RUN apt-get update &amp;&amp; apt-get install -y \

golang-go \

git \

&amp;&amp; rm -rf /var/lib/apt/lists/*

WORKDIR /app

COPY main.go .

RUN go build -o app main.go

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

<p>Tamanho final: ~500MB (Ubuntu base + Go toolchain)</p>

<h3>Exemplo 2: Dockerfile Multistage com Alpine</h3>

<pre><code class="language-dockerfile"># 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 [&quot;./app&quot;]</code></pre>

<p>Tamanho final: ~15-20MB (dependendo das dependências)</p>

<h3>Exemplo 3: Dockerfile com Distroless</h3>

<pre><code class="language-dockerfile"># 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 [&quot;/app&quot;]</code></pre>

<p>Tamanho final: ~20-30MB (distroless base + binário)</p>

<h3>Aplicação Go Exemplo Completa</h3>

<p>Para testar os Dockerfiles acima, use esta aplicação simples:</p>

<pre><code class="language-go">// main.go

package main

import (

&quot;fmt&quot;

&quot;net/http&quot;

&quot;os&quot;

)

func handler(w http.ResponseWriter, r *http.Request) {

fmt.Fprintf(w, &quot;Hello from distroless/alpine!\n&quot;)

}

func main() {

http.HandleFunc(&quot;/&quot;, handler)

port := os.Getenv(&quot;PORT&quot;)

if port == &quot;&quot; {

port = &quot;8080&quot;

}

fmt.Printf(&quot;Server running on port %s\n&quot;, port)

http.ListenAndServe(&quot;:&quot;+port, nil)

}</code></pre>

<p>Compile localmente:</p>

<pre><code class="language-bash">go build -o app main.go</code></pre>

<p>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.</p>

<h2>Segurança em Produção</h2>

<h3>Redução de Superfície de Ataque</h3>

<p>Imagens distroless eliminam a maioria dos vetores de ataque comuns. Sem shell (<code>/bin/sh</code>), um atacante não pode executar comandos mesmo comprometendo a aplicação. Sem <code>apt</code>, <code>yum</code> ou <code>apk</code>, 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.</p>

<p>Compare com Alpine, que ainda inclui <code>apk</code>: um atacante poderia teoricamente usar <code>apk add</code> 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.</p>

<h3>Análise de Vulnerabilidades com Trivy</h3>

<p>Aqui está como avaliar imagens reais:</p>

<pre><code class="language-bash"># 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</code></pre>

<p>Resultados típicos:</p>

<ul>

<li><strong>Ubuntu 22.04</strong>: 100+ vulnerabilidades (muitas críticas)</li>

<li><strong>Alpine 3.18</strong>: 5-10 vulnerabilidades (geralmente baixas)</li>

<li><strong>Distroless</strong>: 0-2 vulnerabilidades (patches mínimos)</li>

</ul>

<h3>Best Practices de Segurança</h3>

<pre><code class="language-dockerfile"># 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=&quot;-w -s&quot; -o app main.go

FROM alpine:3.18

COPY --from=builder /app/app /app

Nunca rode como root

RUN addgroup -g 1000 appuser &amp;&amp; \

adduser -D -u 1000 -G appuser appuser

USER appuser

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

<h2>Quando Usar Cada Uma</h2>

<h3>Use Distroless Quando:</h3>

<ul>

<li>Sua aplicação é um binário compilado (Go, Rust, C compilado estaticamente)</li>

<li>Você não precisa de ferramentas de debug em produção (logs estruturados são suficientes)</li>

<li>Segurança máxima é crítico (aplicações financeiras, dados sensíveis)</li>

<li>Você quer imagens o mais pequenas possível (edge computing, serverless)</li>

</ul>

<h3>Use Alpine Quando:</h3>

<ul>

<li>Você precisa executar comandos em tempo de build (instalar dependências específicas)</li>

<li>Sua linguagem é interpretada (Python, Node.js) e requer binários adicionais</li>

<li>Você ocasionalmente precisa debugar em produção via <code>docker exec</code></li>

<li>A compatibilidade com <code>glibc</code> é necessária (alguns binários legados)</li>

</ul>

<h3>Use Ubuntu/Debian Quando:</h3>

<ul>

<li>Sua aplicação tem dependências complexas que requerem o ecossistema completo</li>

<li>Você está em ambiente legado onde mudanças arriscadas devem ser evitadas</li>

<li>A diferença de 100-500MB não impacta seus custos ou infraestrutura</li>

</ul>

<h2>Exemplo Real: Aplicação Python com Alpine</h2>

<p>Python é um caso mais realista onde Alpine é preferível a distroless, pois a interpretação requer a runtime Python compilada:</p>

<pre><code class="language-dockerfile"># 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 \

&amp;&amp; pip install --no-cache-dir --user -r requirements.txt \

&amp;&amp; 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 &amp;&amp; \

adduser -D -u 1000 -G appuser appuser

USER appuser

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

<p>Aplicação Python correspondente:</p>

<pre><code class="language-python"># app.py

from flask import Flask

import os

app = Flask(__name__)

@app.route(&#039;/&#039;)

def hello():

return &#039;Hello from Alpine!\n&#039;

if __name__ == &#039;__main__&#039;:

port = os.getenv(&#039;PORT&#039;, 8080)

app.run(host=&#039;0.0.0.0&#039;, port=int(port))</code></pre>

<p>Este Dockerfile com Alpine resultará em ~200-250MB, muito menos que ~800MB com Debian.</p>

<h2>Otimizações Adicionais</h2>

<h3>Compilação Estática em Go</h3>

<p>Para distroless ser verdadeiramente eficaz, compile Go sem dependências dinâmicas:</p>

<pre><code class="language-bash">CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -installsuffix cgo -ldflags=&quot;-w -s&quot; -o app main.go</code></pre>

<p>As flags <code>-w -s</code> removem símbolos de debug, reduzindo tamanho do binário em 30-40%.</p>

<h3>Verificação de Tamanhos Localmente</h3>

<pre><code class="language-bash"># 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</code></pre>

<h2>Conclusão</h2>

<p>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.</p>

<p>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 é &quot;melhor&quot; 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.</p>

<h2>Referências</h2>

<ul>

<li><a href="https://github.com/GoogleContainerTools/distroless" target="_blank" rel="noopener noreferrer">Distroless Docker Images - Google</a></li>

<li><a href="https://alpinelinux.org/about/" target="_blank" rel="noopener noreferrer">Alpine Linux Official Documentation</a></li>

<li><a href="https://docs.docker.com/develop/develop-images/dockerfile_best-practices/" target="_blank" rel="noopener noreferrer">Docker Best Practices - Official Documentation</a></li>

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

<li><a href="https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-190.pdf" target="_blank" rel="noopener noreferrer">Container Security Best Practices - NIST</a></li>

</ul>

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

Comentários

Mais em Docker & Kubernetes

Dominando Custom Resource Definitions: Estendendo a API do Kubernetes em Projetos Reais
Dominando Custom Resource Definitions: Estendendo a API do Kubernetes em Projetos Reais

O que são Custom Resource Definitions (CRDs)? Custom Resource Definitions são...

Dominando Docker Compose Avançado: Profiles, Depends On, Healthchecks e Secrets em Projetos Reais
Dominando Docker Compose Avançado: Profiles, Depends On, Healthchecks e Secrets em Projetos Reais

Docker Compose Avançado: Profiles, Depends On, Healthchecks e Secrets Quando...

Como Usar DaemonSets e Jobs em Kubernetes: Agentes e Tarefas em Lote em Produção
Como Usar DaemonSets e Jobs em Kubernetes: Agentes e Tarefas em Lote em Produção

DaemonSets e Jobs em Kubernetes: Agentes e Tarefas em Lote Quando você começa...