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

Docker Compose: Orquestrando Múltiplos Serviços Localmente
Docker Compose: Orquestrando Múltiplos Serviços Localmente

O artigo anterior terminou com um conjunto de comandos docker run que co...

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

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