DevOps

Docker

Guia completo de Docker — conceitos, Dockerfile, Docker Compose, volumes, redes, registry, segurança e boas práticas de produção

Docker empacota aplicações com suas dependências em containers isolados. Cada container usa o kernel do host mas tem seu próprio filesystem, processos e rede. Isso garante que “funciona na minha máquina” seja verdade em qualquer ambiente.


Conceitos Fundamentais

Image: template somente leitura criado a partir de um Dockerfile. Composta por camadas (layers) empilhadas. Cada instrução no Dockerfile cria uma nova layer.

Container: instância em execução de uma image. Tem estado mutável (layer de escrita) mas a image base é imutável.

Layer: cada instrução RUN, COPY, ADD no Dockerfile cria uma layer. Layers são cacheadas — se uma layer não mudou, o Docker reutiliza o cache.

Registry: repositório de images. Docker Hub é o padrão público. Harbor, ECR, GCR são registries privados.

Volume: mecanismo para persistir dados fora do ciclo de vida do container. O container pode ser destruído e recriado sem perder os dados do volume.

Network: rede virtual que conecta containers. Containers na mesma rede se comunicam pelo nome do container (DNS interno).

Layer stack de uma image:
┌─────────────────────────────┐
│  Layer 4: COPY app.jar      │ ← muda em cada build (código)
├─────────────────────────────┤
│  Layer 3: RUN mvn package   │ ← muda quando dependências mudam
├─────────────────────────────┤
│  Layer 2: COPY pom.xml      │ ← muda quando pom.xml muda
├─────────────────────────────┤
│  Layer 1: FROM eclipse-temurin:21│ ← raramente muda (base image)
└─────────────────────────────┘
Cache hit: se a layer não mudou, Docker reutiliza do cache

Dockerfile Básico

Instruções mais usadas com suas diferenças:

# FROM — define a base image
# Use tags específicas, nunca :latest em produção
FROM eclipse-temurin:21-jre-alpine

# WORKDIR — define o diretório de trabalho dentro do container
# Cria o diretório se não existir; evita usar caminhos relativos
WORKDIR /app

# COPY — copia arquivos do host para o container
# Preferido em relação a ADD para arquivos locais
COPY target/app.jar app.jar

# ADD — como COPY mas também extrai .tar e suporta URLs remotas
# Use ADD apenas quando precisar dessas funcionalidades extras
ADD https://example.com/config.tar.gz /config/

# RUN — executa comando durante o build (cria nova layer)
# Combine em uma linha com && para minimizar layers
RUN apt-get update && \
    apt-get install -y curl && \
    rm -rf /var/lib/apt/lists/*

# ENV — define variável de ambiente disponível em runtime
ENV APP_PORT=8080 \
    JAVA_OPTS="-Xmx512m"

# EXPOSE — documenta a porta que o container escuta (não publica de fato)
EXPOSE 8080

# CMD vs ENTRYPOINT — diferença importante:
# CMD — comando padrão, pode ser sobrescrito no docker run
CMD ["java", "-jar", "app.jar"]

# ENTRYPOINT — comando fixo, args do docker run são passados como parâmetros
ENTRYPOINT ["java", "-jar", "app.jar"]
# docker run myapp --spring.profiles.active=prod
# executa: java -jar app.jar --spring.profiles.active=prod

# Combinação comum: ENTRYPOINT fixo + CMD como args default
ENTRYPOINT ["java"]
CMD ["-jar", "app.jar"]
# docker run myapp -jar app.jar                     ← usa CMD
# docker run myapp -Xmx1g -jar app.jar             ← sobrescreve CMD

# HEALTHCHECK — verifica se o container está saudável
HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \
    CMD curl -f http://localhost:8080/actuator/health || exit 1

# LABEL — metadados da image
LABEL maintainer="rafael@empresa.com" \
      version="1.0.0" \
      description="Order Service"

Dockerfile Avançado — Multi-Stage Build

Multi-stage builds reduzem drasticamente o tamanho da image final separando as ferramentas de build das dependências de runtime.

# ═══ STAGE 1: Dependências (cache otimizado) ═══
FROM maven:3.9-eclipse-temurin-21 AS dependencies
WORKDIR /build

# Copia apenas pom.xml primeiro — cache desta layer é válido
# enquanto pom.xml não mudar (mesmo que o código mude)
COPY pom.xml .
RUN mvn dependency:go-offline -q

# ═══ STAGE 2: Build ═══
FROM dependencies AS build
# Agora copia o código-fonte (layer que muda frequentemente)
COPY src ./src
RUN mvn package -DskipTests -q

# ═══ STAGE 3: Runtime (image final enxuta) ═══
FROM eclipse-temurin:21-jre-alpine AS runtime

# Segurança: não rodar como root
RUN addgroup -S appgroup && adduser -S appuser -G appgroup

WORKDIR /app

# Copia apenas o JAR compilado — sem Maven, sem código-fonte, sem dependências dev
COPY --from=build /build/target/app.jar app.jar

# Altera propriedade para o usuário não-root
RUN chown appuser:appgroup app.jar

# Usa o usuário não-root
USER appuser

EXPOSE 8080

HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
    CMD wget -qO- http://localhost:8080/actuator/health/readiness || exit 1

ENTRYPOINT ["java", "-XX:+UseContainerSupport", "-XX:MaxRAMPercentage=75.0", "-jar", "app.jar"]
# Multi-stage para Node.js (React/Vue)
FROM node:22-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --only=production

FROM node:22-alpine AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM nginx:1.27-alpine AS runtime
# Remove config padrão do nginx
RUN rm /etc/nginx/conf.d/default.conf
COPY nginx.conf /etc/nginx/conf.d/

# Copia apenas o build final
COPY --from=build /app/dist /usr/share/nginx/html

# Usuário não-root para nginx
RUN chown -R nginx:nginx /usr/share/nginx/html && \
    chown -R nginx:nginx /var/cache/nginx && \
    touch /var/run/nginx.pid && \
    chown nginx:nginx /var/run/nginx.pid

USER nginx

EXPOSE 80

HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
    CMD wget -qO- http://localhost/health || exit 1

.dockerignore

Evita copiar arquivos desnecessários para o contexto de build, acelerando o build e reduzindo o tamanho da image.

# .dockerignore
# Controle de versão
.git
.gitignore

# Dependências locais (serão instaladas no container)
node_modules
target
.mvn

# Arquivos de desenvolvimento
*.md
.env
.env.local
.env.*.local

# IDE e OS
.idea
.vscode
*.iml
.DS_Store
Thumbs.db

# Logs e temporários
*.log
tmp
.tmp

# Testes
**/__tests__
**/*.test.ts
**/*.spec.ts
coverage

# Docker
Dockerfile*
docker-compose*.yml

Dockerfile Best Practices

Boas práticas para builds mais rápidos, images menores e containers mais seguros:

# ✅ 1. Ordem das instruções — do menos frequente para o mais frequente
# Aproveita o cache do Docker ao máximo

FROM eclipse-temurin:21-jre-alpine

# Estas layers raramente mudam — ficam no início
RUN apk add --no-cache curl

# Esta muda apenas quando pom.xml muda
COPY pom.xml .
RUN mvn dependency:go-offline

# Esta muda a cada build com mudança de código
COPY src ./src
RUN mvn package -DskipTests

# ✅ 2. Minimizar camadas — combine RUN commands
# ❌ Ruim: 3 layers
RUN apt-get update
RUN apt-get install -y curl
RUN rm -rf /var/lib/apt/lists/*

# ✅ Bom: 1 layer
RUN apt-get update && \
    apt-get install -y curl && \
    rm -rf /var/lib/apt/lists/*

# ✅ 3. Usuário não-root
RUN addgroup -S app && adduser -S app -G app
USER app

# ✅ 4. Imagens base específicas (não :latest)
FROM eclipse-temurin:21.0.5_11-jre-alpine   # versão exata
# FROM eclipse-temurin:latest                # ❌ imprevisível

# ✅ 5. HEALTHCHECK em todo container de produção
HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \
    CMD curl -f http://localhost:8080/health || exit 1

# ✅ 6. LABEL para rastreabilidade
LABEL org.opencontainers.image.source="https://github.com/empresa/order-service" \
      org.opencontainers.image.version="1.2.3" \
      org.opencontainers.image.revision="abc1234"

# ✅ 7. Filesystem somente leitura quando possível (runtime flag, não Dockerfile)
# docker run --read-only --tmpfs /tmp myimage

docker run — Flags Importantes

# Flags básicas
docker run \
  -d \                              # detached (background)
  -p 8080:8080 \                    # porta host:container
  -p 127.0.0.1:5432:5432 \         # bind apenas no localhost
  -e SPRING_PROFILES_ACTIVE=prod \ # variável de ambiente
  -e DB_PASSWORD="$(cat secret)" \ # valor de arquivo
  --name order-service \            # nome do container
  --rm \                            # remove ao parar
  --network ecommerce-net \         # conecta à rede
  --restart unless-stopped \        # política de restart
  myimage:1.0.0

# Políticas de restart
--restart no              # nunca reinicia (padrão)
--restart always          # sempre reinicia (inclusive na inicialização do Docker)
--restart unless-stopped  # sempre reinicia, exceto se parado manualmente
--restart on-failure:3    # reinicia em caso de erro, máximo 3 vezes

# Limites de recursos
docker run \
  --memory 512m \          # limite de RAM
  --memory-swap 512m \     # swap igual ao memory = sem swap
  --cpus 0.5 \             # 50% de 1 CPU
  --cpu-shares 512 \       # peso relativo (padrão=1024)
  myimage

# Volumes
docker run \
  -v /host/path:/container/path \      # bind mount
  -v myvolume:/container/path \        # named volume
  -v /container/path \                 # volume anônimo
  --mount type=tmpfs,target=/tmp \     # tmpfs (apenas memória)
  myimage

# Modo interativo e remoção ao sair
docker run -it --rm alpine sh

# Exec em container rodando
docker exec -it container-name sh
docker exec -it container-name bash
docker exec container-name cat /etc/hosts

Docker Compose

# docker-compose.yml
version: '3.9'

services:
  # ═══ API ═══
  api:
    build:
      context: .
      dockerfile: Dockerfile
      target: runtime          # stop em stage específico do multi-stage
      args:
        BUILD_VERSION: "1.0.0" # --build-arg
    image: ecommerce/api:1.0.0
    container_name: ecommerce-api
    ports:
      - "8080:8080"
    environment:
      - SPRING_PROFILES_ACTIVE=docker
      - DB_HOST=postgres
      - DB_PORT=5432
    env_file:
      - .env                   # variáveis de arquivo .env
    depends_on:
      postgres:
        condition: service_healthy   # aguarda healthcheck
      kafka:
        condition: service_started
    networks:
      - backend
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/actuator/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 60s
    deploy:
      resources:
        limits:
          memory: 512m
          cpus: '0.5'

  # ═══ PostgreSQL ═══
  postgres:
    image: postgres:17-alpine
    container_name: ecommerce-postgres
    environment:
      POSTGRES_DB: ecommerce
      POSTGRES_USER: ${DB_USER:-dev}
      POSTGRES_PASSWORD: ${DB_PASSWORD:-dev}
    volumes:
      - postgres_data:/var/lib/postgresql/data
      - ./sql/init:/docker-entrypoint-initdb.d  # scripts de inicialização
    ports:
      - "127.0.0.1:5432:5432"   # apenas localhost
    networks:
      - backend
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-dev} -d ecommerce"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 10s

  # ═══ Redis ═══
  redis:
    image: redis:7-alpine
    container_name: ecommerce-redis
    command: redis-server --appendonly yes --maxmemory 256mb --maxmemory-policy allkeys-lru
    volumes:
      - redis_data:/data
    networks:
      - backend
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 5s
      retries: 3

  # ═══ Kafka ═══
  kafka:
    image: confluentinc/cp-kafka:7.7.0
    container_name: ecommerce-kafka
    environment:
      KAFKA_NODE_ID: 1
      KAFKA_PROCESS_ROLES: broker,controller
      KAFKA_LISTENERS: PLAINTEXT://0.0.0.0:9092,CONTROLLER://0.0.0.0:9093
      KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092
      KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER
      KAFKA_CONTROLLER_QUORUM_VOTERS: 1@kafka:9093
      CLUSTER_ID: MkU3OEVBNTcwNTJENDM2Qk
    volumes:
      - kafka_data:/var/lib/kafka/data
    networks:
      - backend
    profiles:
      - messaging   # só sobe com: docker compose --profile messaging up

volumes:
  postgres_data:
    driver: local
  redis_data:
    driver: local
  kafka_data:
    driver: local

networks:
  backend:
    driver: bridge
    ipam:
      config:
        - subnet: 172.20.0.0/16
# docker-compose.override.yml — sobrescreve para desenvolvimento local
# Aplicado automaticamente quando existe no mesmo diretório
services:
  api:
    build:
      target: development      # usa stage de dev do multi-stage
    volumes:
      - .:/app                  # live reload: monta código-fonte
      - /app/node_modules       # exceção: não monta node_modules do host
    environment:
      - SPRING_DEVTOOLS_RESTART_ENABLED=true
      - DEBUG=true
    ports:
      - "5005:5005"             # porta de debug Java

# Uso:
# docker compose up                    ← aplica override automaticamente
# docker compose -f docker-compose.yml up  ← sem override

Volumes — Quando Usar Cada Tipo

# NAMED VOLUME — dados persistentes gerenciados pelo Docker
# Use para: bancos de dados, uploads, dados que devem sobreviver ao container
docker volume create postgres_data
docker run -v postgres_data:/var/lib/postgresql/data postgres:17

# BIND MOUNT — monta diretório do host
# Use para: código-fonte em desenvolvimento, configs, certificados
docker run -v /home/rafael/app:/app myimage
docker run -v ./nginx.conf:/etc/nginx/nginx.conf:ro nginx  # :ro = somente leitura

# TMPFS — apenas em memória, não persiste
# Use para: dados temporários sensíveis, performance em /tmp
docker run --tmpfs /tmp:size=100m,mode=1777 myimage
# Ou no compose:
# tmpfs:
#   - /tmp:size=100m

# Named volume vs bind mount — comparação:
# Named volume: portátil, gerenciado pelo Docker, sem problemas de permissão entre OS
# Bind mount: acesso direto ao filesystem do host, útil em desenvolvimento

Networks — Tipos e DNS Interno

# BRIDGE (padrão) — containers na mesma rede se comunicam por nome
docker network create --driver bridge ecommerce-net
docker run --network ecommerce-net --name postgres postgres:17
docker run --network ecommerce-net --name api \
    -e DB_HOST=postgres \   # usa o nome do container como hostname!
    myapi

# HOST — container usa a rede do host diretamente (sem isolamento)
# Use para: performance máxima, quando precisar de multicast/broadcast
docker run --network host myimage

# NONE — sem rede
docker run --network none myimage

# OVERLAY — para Docker Swarm (múltiplos hosts)
docker network create --driver overlay --attachable swarm-net

# Inspeção de rede
docker network ls
docker network inspect ecommerce-net

# DNS interno: containers se comunicam pelo nome (apenas na mesma rede)
# postgres:5432, redis:6379, kafka:9092

Docker Build — BuildKit e Cache

# BuildKit — engine de build moderno (padrão no Docker 23+)
export DOCKER_BUILDKIT=1

# Build com tag
docker build -t minha-app:1.0.0 .
docker build -t minha-app:latest -t minha-app:1.0.0 .

# Build com build args (passados via --build-arg)
docker build \
  --build-arg BUILD_VERSION=1.0.0 \
  --build-arg NODE_ENV=production \
  -t minha-app:1.0.0 .

# Build específico de um stage (multi-stage)
docker build --target build -t minha-app:build .
docker build --target runtime -t minha-app:1.0.0 .

# Cache externo — reutiliza cache de CI/CD anterior
docker build \
  --cache-from type=registry,ref=myregistry.com/myapp:cache \
  --cache-to type=registry,ref=myregistry.com/myapp:cache,mode=max \
  -t myapp:1.0.0 .

# Forçar rebuild sem cache
docker build --no-cache -t minha-app:1.0.0 .

# Build multiplataforma (AMD64 + ARM64)
docker buildx build \
  --platform linux/amd64,linux/arm64 \
  -t minha-app:1.0.0 \
  --push .

Registry — Pull, Push e Login

# Login
docker login                              # Docker Hub (pede usuário/senha)
docker login myregistry.homelab.com       # registry privado
docker login -u usuario -p "$TOKEN" registry.exemplo.com

# Pull
docker pull nginx:1.27-alpine
docker pull myregistry.com/myapp:1.0.0

# Tag — necessário antes de push para registry não-padrão
docker tag myapp:1.0.0 myregistry.com/namespace/myapp:1.0.0
docker tag myapp:1.0.0 myregistry.com/namespace/myapp:latest

# Push
docker push myregistry.com/namespace/myapp:1.0.0
docker push myregistry.com/namespace/myapp:latest

# Listar e inspecionar
docker images
docker images --filter dangling=false --filter label=maintainer=rafael
docker image inspect myapp:1.0.0
docker history myapp:1.0.0  # ver as layers e seus tamanhos

Comandos de Inspeção e Diagnóstico

# LOGS
docker logs container-name                  # todos os logs
docker logs -f container-name               # follow (stream)
docker logs --since 1h container-name       # última 1 hora
docker logs --since 2024-01-15T10:00:00 \
            --until 2024-01-15T11:00:00 \
            container-name
docker logs --tail 100 container-name       # últimas 100 linhas

# INSPECT — JSON completo com todas as configs
docker inspect container-name
docker inspect --format '{{.NetworkSettings.IPAddress}}' container-name
docker inspect --format '{{.State.Status}}' container-name
docker inspect --format '{{json .Mounts}}' container-name | jq

# STATS — uso de recursos em tempo real
docker stats                                # todos os containers
docker stats container-name                 # container específico
docker stats --no-stream container-name     # snapshot único (sem stream)
docker stats --format "table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}"

# TOP — processos dentro do container
docker top container-name
docker top container-name aux

# EXEC — executar comando em container rodando
docker exec -it container-name sh
docker exec -it container-name bash
docker exec container-name env             # listar variáveis de ambiente
docker exec container-name ps aux         # processos
docker exec container-name cat /etc/hosts

# COPY — copiar arquivos para/de container
docker cp container-name:/app/logs ./logs  # do container para host
docker cp ./config.yml container-name:/app/config.yml  # do host para container

# DIFF — mudanças no filesystem do container
docker diff container-name

Limpeza — Liberando Espaço em Disco

# Ver uso de disco do Docker
docker system df
docker system df -v  # detalhado

# Limpeza seletiva
docker image prune          # remove images sem tag (dangling)
docker image prune -a       # remove todas images não usadas por containers
docker container prune      # remove containers parados
docker volume prune         # remove volumes não usados
docker network prune        # remove redes sem containers

# Limpeza total (use com cuidado!)
docker system prune         # containers, networks e images dangling
docker system prune -a      # + todas as images não usadas
docker system prune -af --volumes  # tudo, incluindo volumes

# Remover itens específicos
docker rmi myimage:1.0.0
docker rmi $(docker images -f dangling=true -q)  # todas as dangling images
docker rm $(docker ps -aq -f status=exited)       # todos os containers parados

Segurança — Non-Root, Read-Only e Secrets

# ✅ Usuário não-root com UID/GID explícito
RUN groupadd --gid 1001 appgroup && \
    useradd --uid 1001 --gid appgroup --no-create-home appuser

USER 1001:1001  # por UID para evitar dependência de /etc/passwd

# ✅ Filesystem somente leitura (no Dockerfile, via runtime flag)
# docker run --read-only --tmpfs /tmp --tmpfs /var/log myimage
# ✅ SECRETS — não passar senhas via -e (visível em docker inspect)

# Docker Secrets (Swarm mode)
echo "minha_senha_secreta" | docker secret create db_password -
docker service create \
    --secret db_password \
    --env DB_PASSWORD_FILE=/run/secrets/db_password \
    myservice

# Alternativa: montar arquivo com secret
docker run \
    -v /run/secrets/db_password:/run/secrets/db_password:ro \
    -e DB_PASSWORD_FILE=/run/secrets/db_password \
    myimage

# No código, ler do arquivo:
# String password = Files.readString(Path.of(System.getenv("DB_PASSWORD_FILE")));
# docker-compose.yml com secrets
secrets:
  db_password:
    file: ./secrets/db_password.txt

services:
  api:
    secrets:
      - db_password
    environment:
      - DB_PASSWORD_FILE=/run/secrets/db_password
# ✅ Security options
docker run \
    --security-opt no-new-privileges:true \  # previne escalada de privilégios
    --cap-drop ALL \                          # remove todas as capacidades
    --cap-add NET_BIND_SERVICE \             # adiciona apenas o necessário
    --read-only \                            # filesystem somente leitura
    myimage

BuildKit Avançado

Habilitando BuildKit

BuildKit é o backend moderno de build do Docker, com cache granular, builds paralelos e recursos avançados de montagem.

# Habilitar por variável de ambiente (Docker < 23)
DOCKER_BUILDKIT=1 docker build -t myapp .

# Docker 23+ e Docker Desktop: BuildKit é padrão
# Desabilitar explicitamente se necessário:
DOCKER_BUILDKIT=0 docker build .

# Usar buildx (sempre usa BuildKit)
docker buildx build -t myapp .

--mount=type=cache — cache de dependências entre builds

Evita re-baixar dependências a cada build, mesmo quando o layer muda.

# Node.js — cache do npm
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN --mount=type=cache,target=/root/.npm \
    npm ci --prefer-offline
COPY . .
RUN npm run build

# Maven — cache do repositório local
FROM maven:3.9-eclipse-temurin-21
WORKDIR /app
COPY pom.xml .
RUN --mount=type=cache,target=/root/.m2 \
    mvn dependency:go-offline -q
COPY src ./src
RUN --mount=type=cache,target=/root/.m2 \
    mvn package -DskipTests

# Python — cache do pip
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN --mount=type=cache,target=/root/.cache/pip \
    pip install -r requirements.txt
COPY . .

O cache do --mount=type=cache persiste entre builds no host. Não fica na image final — é transparente.

--mount=type=secret — segredos sem expor em layers

Injeta segredos apenas durante o build sem que fiquem registrados em nenhuma layer da image.

# Dockerfile
FROM alpine
# Segredo disponível apenas durante esse RUN, não persiste na image
RUN --mount=type=secret,id=npmrc,target=/root/.npmrc \
    npm install

RUN --mount=type=secret,id=github_token \
    GITHUB_TOKEN=$(cat /run/secrets/github_token) \
    gh release download ...
# Passar o segredo no build
docker buildx build \
    --secret id=npmrc,src=$HOME/.npmrc \
    --secret id=github_token,env=GITHUB_TOKEN \
    -t myapp .

--mount=type=ssh — acesso a repositórios git privados

FROM alpine
RUN apk add --no-cache openssh-client git
# Usa o agente SSH do host — chave nunca entra na image
RUN --mount=type=ssh \
    git clone git@github.com:empresa/repo-privado.git /app
# Iniciar ssh-agent e adicionar chave antes do build
eval $(ssh-agent)
ssh-add ~/.ssh/id_ed25519
docker buildx build --ssh default -t myapp .

Multi-arch com docker buildx build --platform

# Criar e usar um builder multi-plataforma
docker buildx create --name multiarch --use
docker buildx inspect --bootstrap

# Build para múltiplas arquiteturas e push direto ao registry
docker buildx build \
    --platform linux/amd64,linux/arm64,linux/arm/v7 \
    --tag registro.example.com/myapp:1.0.0 \
    --push \
    .

# Build local (sem push) — somente uma plataforma por vez em --load
docker buildx build \
    --platform linux/arm64 \
    --tag myapp:arm64 \
    --load \
    .

docker buildx bake — builds complexos declarativos

# docker-bake.hcl
variable "TAG" {
  default = "latest"
}

group "default" {
  targets = ["api", "worker", "frontend"]
}

target "api" {
  context    = "./api"
  dockerfile = "Dockerfile"
  platforms  = ["linux/amd64", "linux/arm64"]
  tags       = ["registro.example.com/api:${TAG}"]
  cache-from = ["type=registry,ref=registro.example.com/api:cache"]
  cache-to   = ["type=registry,ref=registro.example.com/api:cache,mode=max"]
}

target "worker" {
  context = "./worker"
  tags    = ["registro.example.com/worker:${TAG}"]
}

target "frontend" {
  context = "./frontend"
  args = {
    NODE_ENV = "production"
  }
  tags = ["registro.example.com/frontend:${TAG}"]
}
# Executar todos os targets
TAG=1.2.0 docker buildx bake --push

# Executar target específico
docker buildx bake api --push

Image Signing com Docker Content Trust

Docker Content Trust (DCT)

DCT usa o Notary para assinar e verificar images. Quando habilitado, apenas images assinadas podem ser pulled/run.

# Habilitar DCT globalmente (afeta push e pull)
export DOCKER_CONTENT_TRUST=1

# Push assina automaticamente (cria chaves se não existirem)
docker push meuregistry.com/myapp:1.0.0

# Pull verifica assinatura — falha se não assinada
docker pull meuregistry.com/myapp:1.0.0

# Inspecionar confiança de uma image
docker trust inspect --pretty meuregistry.com/myapp:1.0.0

# Assinar image já existente no registry
docker trust sign meuregistry.com/myapp:1.0.0

# Adicionar signatário ao repositório
docker trust signer add --key cert.pem ci-bot meuregistry.com/myapp

# Revogar assinatura
docker trust revoke meuregistry.com/myapp:1.0.0

Cosign — alternativa moderna (Sigstore)

Cosign é a solução recomendada atualmente para assinar imagens OCI. Não depende de Notary e integra com OIDC.

# Instalação
brew install cosign          # macOS
go install github.com/sigstore/cosign/v2/cmd/cosign@latest

# Gerar par de chaves
cosign generate-key-pair
# Gera cosign.key (privada) e cosign.pub (pública)

# Assinar image (requer que a image já esteja no registry)
cosign sign --key cosign.key registro.example.com/myapp:1.0.0

# Verificar assinatura
cosign verify --key cosign.pub registro.example.com/myapp:1.0.0

# Assinar sem chave local — usando OIDC (keyless signing)
# Exige acesso ao Fulcio/Rekor (infra Sigstore pública ou self-hosted)
COSIGN_EXPERIMENTAL=1 cosign sign registro.example.com/myapp:1.0.0

Assinatura em CI/CD com cosign

# .github/workflows/release.yml
- name: Build and push
  uses: docker/build-push-action@v5
  with:
    push: true
    tags: registro.example.com/myapp:${{ github.sha }}

- name: Sign image with cosign
  env:
    COSIGN_PRIVATE_KEY: ${{ secrets.COSIGN_PRIVATE_KEY }}
    COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }}
  run: |
    cosign sign --key env://COSIGN_PRIVATE_KEY \
      registro.example.com/myapp:${{ github.sha }}

- name: Verify before deploy
  run: |
    cosign verify --key cosign.pub \
      registro.example.com/myapp:${{ github.sha }}
# Verificar policy em produção com cosign policy
cosign verify \
    --certificate-identity "https://github.com/org/repo/.github/workflows/release.yml@refs/heads/main" \
    --certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
    registro.example.com/myapp:1.0.0

Segurança — seccomp e AppArmor

Seccomp — filtragem de syscalls

Seccomp (Secure Computing Mode) cria um filtro de chamadas de sistema que o container pode fazer. O Docker tem um perfil padrão que bloqueia ~44 syscalls perigosas.

# Ver perfil seccomp padrão do Docker
docker run --rm -it ubuntu cat /proc/1/status | grep Seccomp
# Seccomp: 2  → 2 = FILTER (ativo), 0 = desabilitado

# Desabilitar seccomp (não recomendado em produção)
docker run --security-opt seccomp=unconfined myimage

# Usar perfil customizado
docker run --security-opt seccomp=/path/to/profile.json myimage
// profile.json — perfil seccomp customizado (baseado no padrão + bloqueios extras)
{
  "defaultAction": "SCMP_ACT_ERRNO",
  "syscalls": [
    {
      "names": [
        "read", "write", "open", "close", "stat", "fstat",
        "mmap", "mprotect", "munmap", "brk", "rt_sigaction",
        "rt_sigprocmask", "ioctl", "access", "pipe", "select",
        "sched_yield", "mremap", "msync", "mincore", "madvise",
        "dup", "dup2", "nanosleep", "getpid", "socket", "connect",
        "accept", "sendto", "recvfrom", "bind", "listen",
        "getsockname", "getpeername", "socketpair", "setsockopt",
        "getsockopt", "clone", "fork", "execve", "exit", "wait4",
        "kill", "getdents", "getcwd", "chdir", "mkdir", "rmdir",
        "unlink", "rename", "chmod", "chown", "getuid", "getgid",
        "setuid", "setgid", "getgroups", "futex", "prctl", "arch_prctl",
        "exit_group", "epoll_wait", "epoll_ctl", "tgkill", "openat",
        "newfstatat", "pread64", "pwrite64", "sendmsg", "recvmsg"
      ],
      "action": "SCMP_ACT_ALLOW"
    }
  ]
}

Syscalls bloqueadas pelo perfil padrão incluem: reboot, mount, umount, kexec_load, create_module, init_module, delete_module, ptrace (parcialmente), perf_event_open, entre outras.

AppArmor — controle de acesso mandatório (MAC)

AppArmor restringe o que o processo pode acessar no sistema de arquivos, rede e capabilities — em nível de kernel.

# Docker aplica o perfil "docker-default" automaticamente em sistemas Linux com AppArmor
docker run --security-opt apparmor=docker-default myimage

# Desabilitar AppArmor (apenas para debug)
docker run --security-opt apparmor=unconfined myimage

# Usar perfil customizado
# 1. Criar o perfil
cat > /etc/apparmor.d/containers/myapp-profile << 'EOF'
#include <tunables/global>
profile myapp-profile flags=(attach_disconnected,mediate_deleted) {
  #include <abstractions/base>
  network inet tcp,
  network inet udp,
  deny /etc/shadow r,
  deny /root/** rw,
  /app/** r,
  /tmp/** rw,
}
EOF

# 2. Carregar o perfil
apparmor_parser -r -W /etc/apparmor.d/containers/myapp-profile

# 3. Usar no container
docker run --security-opt apparmor=myapp-profile myimage

Capabilities — princípio do menor privilégio

Linux divide os privilégios de root em ~40 capabilities granulares. Por padrão, o Docker mantém um subconjunto pequeno.

# Remover TODAS as capabilities e adicionar apenas o necessário
docker run \
    --cap-drop ALL \
    --cap-add NET_BIND_SERVICE \   # bind em portas < 1024
    myimage

# Capabilities mais comuns e seus usos:
# NET_BIND_SERVICE  → bind em portas privilegiadas (< 1024)
# NET_ADMIN         → configuração de rede (iptables, interfaces)
# SYS_PTRACE        → debug de processos (strace, gdb)
# SYS_ADMIN         → operações de sistema (mount, namespaces) — muito amplo, evitar
# CHOWN             → mudar ownership de arquivos
# DAC_OVERRIDE      → ignorar permissões de arquivos
# SETUID / SETGID   → trocar uid/gid do processo

# Ver capabilities atuais do container
docker run --rm ubuntu capsh --print

Rootless Docker

Rootless Docker executa o daemon e containers sem privilégios de root no host. Recomendado para ambientes multi-usuário.

# Instalar rootless Docker
dockerd-rootless-setuptool.sh install

# Usar rootless Docker
export DOCKER_HOST=unix://$XDG_RUNTIME_DIR/docker.sock
docker run hello-world

# Configurar como serviço do usuário (systemd)
systemctl --user enable docker
systemctl --user start docker

Instrução USER no Dockerfile

FROM eclipse-temurin:21-jre-alpine

WORKDIR /app
COPY target/app.jar app.jar

# Criar usuário não-root dedicado
RUN addgroup -S appgroup && adduser -S appuser -G appgroup

# Ajustar permissões antes de trocar de usuário
RUN chown -R appuser:appgroup /app

# Trocar para usuário não-root — todas as instruções seguintes e o CMD rodam como appuser
USER appuser

EXPOSE 8080
CMD ["java", "-jar", "app.jar"]
# Verificar qual usuário o container está rodando
docker run --rm myimage whoami
# appuser  ← não deve retornar root

# Inspecionar usuário configurado na image
docker inspect myimage --format '{{.Config.User}}'
# docker-compose.yml — definir usuário em runtime
services:
  api:
    image: myapp:latest
    user: "1001:1001"          # uid:gid explícito
    read_only: true             # filesystem somente leitura
    security_opt:
      - no-new-privileges:true  # impede escalada de privilégios via setuid
    cap_drop:
      - ALL
    cap_add:
      - NET_BIND_SERVICE