Docker e Contêineres

Contêineres, imagens, boas práticas de Dockerfile e multi-stage builds para empacotar e distribuir aplicações.

O que é Docker?

Docker é uma plataforma que permite empacotar aplicações junto com todas as suas dependências em unidades isoladas chamadas contêineres. Um contêiner inclui o código, o runtime, as bibliotecas e a configuração necessária para que a aplicação funcione de forma idêntica em qualquer máquina.

Diferentemente das máquinas virtuais, os contêineres compartilham o kernel do sistema operacional host, o que os torna muito mais leves e rápidos de iniciar.

Conceitos fundamentais

Imagem

Uma imagem é um pacote imutável que contém tudo o que é necessário para executar uma aplicação. Ela é construída a partir de um Dockerfile e armazenada em um registry (como Docker Hub, ECR ou GCR).

As imagens são compostas por camadas (layers). Cada instrução do Dockerfile cria uma nova camada. O Docker armazena em cache as camadas que não mudam, o que acelera as builds seguintes.

Contêiner

Um contêiner é uma instância em execução de uma imagem. É efêmero por natureza — quando é interrompido, seu estado interno é perdido (a menos que se usem volumes).

Registry

Um registry é um repositório de imagens. Os mais comuns são:

  • Docker Hub: registry público padrão
  • Amazon ECR: registry privado na AWS
  • Google GCR / Artifact Registry: registry no GCP
  • GitHub Container Registry: integrado com o GitHub

Dockerfile: boas práticas

O Dockerfile define como uma imagem é construída. Um Dockerfile bem escrito produz imagens pequenas, seguras e rápidas de construir.

Exemplo básico

FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
EXPOSE 3000
CMD ["node", "dist/server.js"]

Boas práticas

  1. Usar imagens base pequenas: preferir variantes alpine ou slim em vez das imagens completas
  2. Ordenar instruções por frequência de mudança: as camadas que mudam menos vêm primeiro para aproveitar o cache
  3. Copiar dependências antes do código: COPY package*.json antes de COPY . para cachear o npm install
  4. Não executar como root: criar um usuário sem privilégios com USER
  5. Usar .dockerignore: excluir node_modules, .git, arquivos de teste e documentação
  6. Minimizar o número de camadas: combinar comandos RUN relacionados com &&
  7. Não instalar ferramentas desnecessárias: cada pacote extra aumenta a superfície de ataque

Exemplo com usuário não root

FROM node:20-alpine
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
WORKDIR /app
COPY --chown=appuser:appgroup package*.json ./
RUN npm ci --only=production
COPY --chown=appuser:appgroup . .
USER appuser
EXPOSE 3000
CMD ["node", "dist/server.js"]

Multi-stage builds

Os multi-stage builds permitem usar múltiplas etapas em um único Dockerfile. Isso é especialmente útil para separar a etapa de compilação da etapa de execução, produzindo imagens finais muito menores.

Por que usar multi-stage?

  • A imagem final não inclui ferramentas de compilação (TypeScript compiler, build tools, etc.)
  • O tamanho da imagem é reduzido drasticamente
  • A superfície de ataque em produção é minimizada

Exemplo: aplicação Node.js com TypeScript

# Etapa 1: Build
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# Etapa 2: Produção
FROM node:20-alpine AS production
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package*.json ./
RUN npm ci --only=production
USER appuser
EXPOSE 3000
CMD ["node", "dist/server.js"]

Neste exemplo, a imagem final contém apenas o código compilado e as dependências de produção — sem TypeScript, sem devDependencies, sem código-fonte.

Docker Compose para desenvolvimento

O Docker Compose permite definir e executar aplicações multi-contêiner. É ideal para o ambiente de desenvolvimento local:

version: '3.8'
services:
  app:
    build: .
    ports:
      - "3000:3000"
    environment:
      - DATABASE_URL=postgres://postgres:password@db:5432/myapp
    depends_on:
      - db
      - redis

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: myapp
      POSTGRES_PASSWORD: password
    volumes:
      - pgdata:/var/lib/postgresql/data

  redis:
    image: redis:7-alpine

volumes:
  pgdata:

Segurança em contêineres

  • Escanear imagens: usar ferramentas como Trivy, Snyk ou Docker Scout para detectar vulnerabilidades
  • Atualizar imagens base: manter as imagens base atualizadas com patches de segurança
  • Não armazenar segredos em imagens: usar variáveis de ambiente ou gerenciadores de segredos em runtime
  • Limitar recursos: configurar limites de CPU e memória para evitar que um contêiner consuma todos os recursos do host
  • Usar imagens assinadas: verificar a procedência das imagens com Docker Content Trust

Resumo

O Docker simplifica o empacotamento e a distribuição de aplicações ao garantir que o mesmo artefato funcione em qualquer ambiente. As chaves são: imagens pequenas com multi-stage builds, boas práticas de Dockerfile, segurança desde o design e Docker Compose para o desenvolvimento local.