DevOps

Boas Práticas de Imagens: Leveza, Segurança e Camadas Já leu

9 min de leitura

Boas Práticas de Imagens: Leveza, Segurança e Camadas
Nos artigos anteriores foram construídas imagens funcionais. Uma imagem funcional resolve o problema imediato — a aplicação sobe e re

Nos artigos anteriores foram construídas imagens funcionais. Uma imagem funcional resolve o problema imediato — a aplicação sobe e responde. Uma imagem de produção vai além: ela é pequena o suficiente para ser transferida rapidamente, segura o suficiente para não expor superfícies de ataque desnecessárias e organizada o suficiente para ser construída em segundos no pipeline de CI/CD.

A diferença entre as duas não está no que a imagem faz, mas em como foi construída. Este artigo sistematiza as práticas que separam uma da outra.


Princípio 1: Escolher a Imagem Base Correta

A imagem base determina o tamanho inicial e a superfície de ataque da imagem final. Três categorias principais:

Imagens completas — como ubuntu:22.04 ou debian:bookworm. Incluem um sistema operacional completo com centenas de utilitários. São convenientes para desenvolvimento e depuração, mas desnecessariamente pesadas para produção.

Imagens slim — como node:20-slim ou python:3.12-slim. São versões reduzidas das imagens oficiais, sem pacotes desnecessários. Um bom equilíbrio entre conveniência e tamanho.

Imagens Alpine — baseadas no Alpine Linux, que ocupa apenas 5MB. Como node:20-alpine ou nginx:alpine. Resultam nas menores imagens possíveis, mas podem exigir ajustes — o Alpine usa musl libc em vez de glibc, o que ocasionalmente causa incompatibilidades com bibliotecas nativas.

Imagens distroless — desenvolvidas pelo Google, contêm apenas o runtime necessário, sem shell, sem gerenciador de pacotes, sem nenhum utilitário de sistema. São as mais seguras para produção porque não oferecem superfície para execução de comandos arbitrários se o container for comprometido.

Comparação prática de tamanhos para uma aplicação Node.js simples:

node:20              →  ~1.1GB
node:20-slim         →  ~240MB
node:20-alpine       →  ~180MB
gcr.io/distroless/nodejs20-debian12  →  ~160MB

A escolha ideal depende do contexto: Alpine para a maioria dos casos em produção, distroless para sistemas de alta segurança, imagens completas apenas para desenvolvimento e depuração.


Princípio 2: Minimizar o Número de Camadas

Cada instrução RUN, COPY e ADD cria uma camada. Camadas desnecessárias aumentam o tamanho da imagem e o tempo de build. A prática recomendada é encadear comandos relacionados em uma única instrução RUN:

Ineficiente — quatro camadas separadas:

RUN apt-get update
RUN apt-get install -y curl
RUN apt-get install -y git
RUN apt-get clean

Eficiente — uma única camada:

RUN apt-get update \
    && apt-get install -y --no-install-recommends \
       curl \
       git \
    && apt-get clean \
    && rm -rf /var/lib/apt/lists/*

O --no-install-recommends evita a instalação de pacotes sugeridos que não são obrigatórios. O rm -rf /var/lib/apt/lists/* remove o cache do apt na mesma camada — se fosse em uma instrução separada, o cache já teria sido persistido na camada anterior e o espaço não seria recuperado.


Princípio 3: Não Rodar como Root

Por padrão, processos dentro de containers rodam como root. Se um atacante explorar uma vulnerabilidade na aplicação e conseguir executar comandos arbitrários dentro do container, terá privilégios de root dentro dele — o que facilita tentativas de escape para o host.

A solução é criar um usuário não-privilegiado e usá-lo para executar a aplicação:

FROM node:20-alpine

WORKDIR /app

COPY package*.json ./
RUN npm ci --only=production

COPY . .

# Cria usuário e grupo dedicados
RUN addgroup -S appgroup && adduser -S appuser -G appgroup

# Define dono dos arquivos
RUN chown -R appuser:appgroup /app

# Muda para o usuário não-privilegiado
USER appuser

EXPOSE 3000
CMD ["node", "dist/index.js"]

A imagem node já inclui um usuário chamado node que pode ser usado diretamente:

# Forma simplificada usando o usuário já existente na imagem base
USER node

Princípio 4: Não Incluir Segredos na Imagem

Um erro comum é passar segredos como variáveis de ambiente durante o build ou copiá-los para dentro da imagem. Mesmo que removidos em camadas subsequentes, os segredos ficam acessíveis no histórico da imagem:

# Isso expõe o token no histórico da imagem
docker history minha-imagem

A forma correta de lidar com segredos durante o build é usar o mecanismo de build secrets do Docker BuildKit:

# syntax=docker/dockerfile:1
FROM node:20-alpine AS builder

WORKDIR /app

COPY package*.json ./

# O secret é montado temporariamente e não persiste na imagem
RUN --mount=type=secret,id=npm_token \
    NPM_TOKEN=$(cat /run/secrets/npm_token) \
    npm ci
# Passa o secret durante o build
docker build \
  --secret id=npm_token,src=$HOME/.npmrc \
  -t minha-app:1.0.0 .

Em runtime, segredos devem ser injetados via variáveis de ambiente pelo orquestrador — nunca embutidos na imagem.


Princípio 5: Usar .dockerignore Rigorosamente

O .dockerignore evita que arquivos desnecessários entrem no contexto de build. Um arquivo bem configurado reduz o tempo de transferência do contexto e evita que informações sensíveis sejam incluídas acidentalmente:

# Controle de versão
.git
.gitignore

# Dependências — serão instaladas durante o build
node_modules
vendor

# Arquivos de ambiente — nunca devem entrar na imagem
.env
.env.*
*.pem
*.key

# Artefatos de build locais
dist
build
coverage
.cache

# Documentação e arquivos de desenvolvimento
*.md
docs
.vscode
.idea

# Logs
*.log
logs

# O próprio Dockerfile e dockerignore
Dockerfile*
.dockerignore

Princípio 6: Fixar Versões de Dependências

Usar tags latest ou omitir versões em imagens base é uma prática que compromete a reprodutibilidade do build:

# Problemático — o que é 'latest' hoje pode não ser amanhã
FROM node:latest
RUN apt-get install -y curl

# Correto — versão específica e reproduzível
FROM node:20.11.1-alpine3.19
RUN apt-get install -y curl=8.2.1-*

Fixar versões garante que o mesmo Dockerfile produz a mesma imagem independentemente de quando é executado. Em pipelines de CI/CD isso é especialmente importante — um build que passou na sexta-feira não deve quebrar na segunda porque uma dependência foi atualizada no fim de semana.


Princípio 7: Verificar Vulnerabilidades

Imagens são compostas por dezenas de pacotes, cada um com seu próprio histórico de vulnerabilidades. A verificação de segurança deve fazer parte do pipeline de build.

O Docker Scout é a ferramenta integrada ao Docker CLI para essa finalidade:

# Analisa a imagem local
docker scout cves minha-app:1.0.0

# Exibe um resumo rápido
docker scout quickview minha-app:1.0.0

# Compara com a versão anterior
docker scout compare minha-app:1.0.0 --to minha-app:0.9.0

O Trivy é uma alternativa open source amplamente usada em pipelines de CI:

# Instala o Trivy
curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh

# Escaneia a imagem
trivy image minha-app:1.0.0

# Falha o build se encontrar vulnerabilidades críticas
trivy image --exit-code 1 --severity CRITICAL minha-app:1.0.0

Integrado ao GitHub Actions:

- name: Escaneia vulnerabilidades
  uses: aquasecurity/trivy-action@master
  with:
    image-ref: minha-app:1.0.0
    format: table
    exit-code: 1
    severity: CRITICAL,HIGH

Um Dockerfile Seguindo Todos os Princípios

# syntax=docker/dockerfile:1
# Versões fixas para reprodutibilidade
FROM node:20.11.1-alpine3.19 AS base

# Instala apenas o necessário em uma única camada
RUN apk add --no-cache tini dumb-init

# ── Dependências ──────────────────────────────────
FROM base AS deps

WORKDIR /app

COPY package*.json ./

# Build secret para registros privados
RUN --mount=type=secret,id=npm_token \
    --mount=type=cache,target=/root/.npm \
    npm ci --only=production

# ── Build ─────────────────────────────────────────
FROM base AS builder

WORKDIR /app

COPY package*.json ./
RUN --mount=type=cache,target=/root/.npm \
    npm ci

COPY . .
RUN npm run build

# ── Produção ──────────────────────────────────────
FROM base AS production

WORKDIR /app

ENV NODE_ENV=production
ENV PORT=3000

# Copia dependências de produção do estágio deps
COPY --from=deps /app/node_modules ./node_modules

# Copia apenas o código compilado
COPY --from=builder /app/dist ./dist

# Define permissões antes de mudar o usuário
RUN chown -R node:node /app

# Usuário não-privilegiado
USER node

EXPOSE 3000

# Healthcheck
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
    CMD wget -qO- http://localhost:3000/health || exit 1

# tini como PID 1 — gerencia sinais corretamente
ENTRYPOINT ["/sbin/tini", "--"]
CMD ["node", "dist/index.js"]

O Que Vem a Seguir

No próximo e último artigo do Módulo 3 será abordada a publicação de imagens em registros — Docker Hub, GitHub Container Registry e registros privados na AWS. É o elo entre a construção local e a distribuição para ambientes de produção.


Referências para Aprofundamento

Documentação oficial - Best Practices for Writing Dockerfiles — docs.docker.com — Guia oficial de boas práticas do Docker, cobrindo cache de camadas, tamanho e segurança. - Docker Scout Documentation — docs.docker.com — Documentação completa da ferramenta de análise de vulnerabilidades integrada ao Docker.

Segurança - Trivy — aquasecurity.github.io — Documentação completa do Trivy, o scanner de vulnerabilidades open source mais usado em pipelines de CI/CD. - Distroless Images — GitHub — Repositório oficial das imagens distroless do Google com exemplos de uso para Node.js, Python, Java e Go.

Leitura complementar - Hadolint — GitHub — Linter para Dockerfiles que verifica automaticamente violações de boas práticas. Pode ser usado localmente ou integrado ao pipeline de CI como step obrigatório.

Comentários

Mais em DevOps

Geradores de Senhas Seguras: Implementações em 5 Linguagens de Programação
Geradores de Senhas Seguras: Implementações em 5 Linguagens de Programação

A geração de senhas fortes é um dos pilares da segurança da informação. Uma s...

Git na Prática: Commits, Branches e Merges sem Medo
Git na Prática: Commits, Branches e Merges sem Medo

Antes do Git, equipes de desenvolvimento compartilhavam código por e-m...

Instalação Manual do MySQL no Debian, Arch, Fedora e openSUSE
Instalação Manual do MySQL no Debian, Arch, Fedora e openSUSE

Este guia cobre a instalação do MySQL 9.7.0 puro (sem MariaDB, sem pacotes de...