Containers · Internals, Build & Otimização

Containers Avançado
Internals, Build & Otimização

Um aprofundamento em como containers realmente funcionam por dentro — OverlayFS, Copy-on-Write, namespaces e cgroups — e como aplicar isso na prática: builds multi-stage e cache avançado com BuildKit, drivers de rede e ECS Service Connect, Task Definitions e Capacity Providers, e técnicas de redução de imagem com lifecycle policies e scanning no ECR.

Overlay Filesystem & Copy-on-Write

Como o Docker armazena dados: OverlayFS

O kernel Linux expõe uma abstração chamada Union Mount — empilhar múltiplos diretórios em uma única visão coesa. O Docker usa a implementação moderna disso: OverlayFS (overlay2 driver).

Quando você faz docker run, o Docker monta quatro camadas:

  • lowerdir → camadas da imagem (read-only, compartilhadas entre containers)
  • upperdir → camada de escrita exclusiva deste container (read-write)
  • workdir → diretório de trabalho interno do OverlayFS (bookkeeping)
  • merged → o que o processo dentro do container enxerga (union das anteriores)
SHELL
/var/lib/docker/overlay2/
├── <layer-sha>/
│   ├── diff/        ← conteúdo real da camada
│   ├── link         ← symlink curto (otimização de kernel)
│   └── lower        ← referência à camada inferior
└── <container-id>/
    ├── diff/        ← upperdir (writes do container)
    ├── merged/      ← mount point final
    └── work/        ← workdir do OverlayFS

Copy-on-Write (CoW)

Arquivos das camadas inferiores são compartilhados entre todos os containers. Só quando um container modifica um arquivo é que uma cópia privada é criada no upperdir dele.
SHELL
# 3 containers rodando a mesma imagem nginx (200MB)
# Uso real em disco: ~200MB + delta de cada container
# SEM CoW seria: 3 × 200MB = 600MB

Quando o container escreve em /etc/nginx/nginx.conf: OverlayFS intercepta a syscall, copia o arquivo para o upperdir do container, a modificação acontece só na cópia privada, e a imagem original fica intacta.

Por que isso importa em produção

⚠️ Containers são efêmeros

Qualquer escrita no upperdir some quando o container morre. No ECS Fargate, o upperdir existe só enquanto a Task existe. Para persistência: EFS mount ou S3 via código.

DOCKERFILE
# Nunca confie no upperdir para dados persistentes
# Logs, uploads, checkpoints LangGraph → sempre em volume
VOLUME ["/app/checkpoints"]

Namespaces & cgroups: o que realmente isola containers

Containers não são VMs — são processos isolados

Um container é literalmente um processo Linux com ilusões de isolamento criadas pelo kernel. Dois mecanismos fazem isso: namespaces (visibilidade) e cgroups (recursos).

Namespaces — isolamento de visibilidade

NamespaceO que isolaExemplo
pidÁrvore de processosPID 1 dentro ≠ PID 1 do host
netStack de redeInterface eth0 própria, rotas próprias
mntSistema de arquivosO que o container enxerga como /
utsHostnameContainer tem seu próprio hostname
ipcIPC (semáforos, filas)Não acessa IPC do host
userUIDs/GIDsUID 0 pode não ser root no host
cgroupVisão dos cgroupsEnxerga só seus próprios limites
timeClock do sistemaPode ter clock offset diferente
BASH
# Inspecionar os namespaces de um processo
ls -la /proc/<pid>/ns/

# Entrar no namespace de um container manualmente
nsenter -t <pid> --net --pid -- bash

cgroups — isolamento de recursos

BASH
docker run --memory=512m --cpus=0.5 minha-app

# O Docker cria cgroups em:
/sys/fs/cgroup/memory/docker/<container-id>/memory.limit_in_bytes
/sys/fs/cgroup/cpu/docker/<container-id>/cpu.cfs_quota_us
JSON
{
  "containerDefinitions": [{
    "memory": 512,            // hard limit via cgroup → OOM kill se ultrapassar
    "memoryReservation": 256, // soft limit para o scheduler do ECS
    "cpu": 512                // 512 units = 0.5 vCPU
  }]
}

O processo PID 1 e por que importa

DOCKERFILE
# ❌ Ruim: shell vira PID 1, não repassa SIGTERM
CMD python app.py

# ✅ Bom: python vira PID 1 diretamente (exec form)
CMD ["python", "app.py"]

# ✅ Melhor: tini como init mínimo
RUN apk add --no-cache tini
ENTRYPOINT ["/sbin/tini", "--"]
CMD ["python", "app.py"]
ECS + SIGTERM + Checkpointing

Se o container não responde ao SIGTERM, o ECS espera o stopTimeout (padrão 30s) e manda SIGKILL — fatal para checkpoints em progresso. Use linuxParameters.initProcessEnabled: true na Task Definition para habilitar init process via Fargate sem precisar do tini no Dockerfile.

OverlayFSoverlay2Copy-on-Write NamespacescgroupsPID 1 SIGTERMtiniFargate

Multi-stage builds

O problema que multi-stage resolve

Imagens de produção carregam peso desnecessário: compiladores, ferramentas de build, arquivos de teste, dependências de dev. Multi-stage separa o ambiente de build do artefato final.

DOCKERFILE
# ── Stage 1: Builder ──────────────────────────────────
FROM python:3.12 AS builder
WORKDIR /build
RUN pip install poetry
COPY pyproject.toml poetry.lock ./
RUN poetry export -f requirements.txt --without dev -o requirements.txt

# ── Stage 2: Runtime ──────────────────────────────────
FROM python:3.12-slim AS runtime
WORKDIR /app
COPY --from=builder /build/requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY src/ ./src/
RUN useradd -r -u 1001 appuser
USER appuser
CMD ["python", "-m", "uvicorn", "src.main:app", "--host", "0.0.0.0"]
Ganho típico

Imagem vai de ~1.2GB para ~180MB. Cold start no ECS Fargate pode ser 2–3 minutos mais rápido.

Padrão para Go — imagem scratch

DOCKERFILE
FROM golang:1.22 AS builder
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
# CGO_ENABLED=0 → binário estático, sem dependência de libc
RUN CGO_ENABLED=0 GOOS=linux go build -o /app ./cmd/server

# Imagem final: SCRATCH — literalmente vazia
FROM scratch
COPY --from=builder /app /app
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
ENTRYPOINT ["/app"]

Imagem resultante: ~8MB. Nenhuma shell, nenhum utilitário, só o binário.

Targets nomeados — builds condicionais

DOCKERFILE
FROM python:3.12-slim AS base
RUN pip install -r requirements.txt

FROM base AS dev
RUN pip install pytest pytest-asyncio
CMD ["pytest"]

FROM base AS production
COPY src/ ./src/
CMD ["uvicorn", "src.main:app"]
BASH
# CI roda testes
docker build --target dev -t app:test .
docker run app:test

# Deploy usa produção
docker build --target production -t app:prod .

BuildKit: cache avançado e paralelismo

BuildKit — o novo engine de build

BuildKit (padrão desde Docker 23) traz: DAG paralelo (stages independentes buildam em paralelo), cache mounts persistentes entre builds, secret mounts que nunca ficam em layers, e SSH forwarding para repos privados.

BASH
# Habilitar em versões antigas do Docker
DOCKER_BUILDKIT=1 docker build .

Cache mounts — o recurso mais subutilizado

DOCKERFILE
# syntax=docker/dockerfile:1

# ❌ pip baixa tudo do zero a cada build
RUN pip install -r requirements.txt

# ✅ cache mount: pip cache persiste no host entre builds
RUN --mount=type=cache,target=/root/.cache/pip \
    pip install -r requirements.txt

# npm
RUN --mount=type=cache,target=/root/.npm \
    npm ci --prefer-offline

# cargo (Rust)
RUN --mount=type=cache,target=/usr/local/cargo/registry \
    cargo build --release
Resultado real

Rebuild após mudar só o código (sem mudar deps) vai de 3 min para 8s.

Secret mounts — nunca exponha credentials em layers

DOCKERFILE
# ❌ NUNCA — o token fica gravado na layer para sempre
RUN pip install git+https://token@github.com/org/pkg.git

# ✅ Secret mount: disponível só durante o RUN, nunca gravado
RUN --mount=type=secret,id=gh_token \
    pip install git+https://$(cat /run/secrets/gh_token)@github.com/org/pkg.git
BASH
docker build --secret id=gh_token,src=~/.github_token .

Cache no CI — ECR como cache backend

BASH
docker buildx build \
  --cache-to   type=registry,ref=<account>.dkr.ecr.<region>.amazonaws.com/cache:buildcache,mode=max \
  --cache-from type=registry,ref=<account>.dkr.ecr.<region>.amazonaws.com/cache:buildcache \
  --push -t <account>.dkr.ecr.<region>.amazonaws.com/app:latest .
mode=max

Exporta cache de todos os stages, não só do final. Crítico para multi-stage builds em CI.

.dockerignore — o que a maioria esquece

.DOCKERIGNORE
.git
.github
**/__pycache__
**/*.pyc
**/.pytest_cache
.env*
tests/
docs/
.venv
node_modules

O contexto de build é enviado ao daemon antes do build começar. Um .git de 500MB enviado toda vez é um desperdício visível no output "Sending build context".

Multi-stageBuildKitcache mount secret mountECR cachemode=max scratch.dockerignore

Drivers de rede: bridge, host, overlay

Como o Docker cria redes

Quando o Docker inicia, ele cria uma interface virtual docker0 no host — um switch virtual Layer 2. Containers se conectam via veth pairs (virtual ethernet).

SHELL
Host:
  ├── eth0     (interface física,  IP: 192.168.1.10)
  ├── docker0  (bridge virtual,   IP: 172.17.0.1)
  │   ├── veth3a4f → container1  (eth0: 172.17.0.2)
  │   └── veth8b2c → container2  (eth0: 172.17.0.3)
  └── iptables rules (NAT: container → internet)

Bridge network — default vs user-defined

BASH
docker network create minha-rede
docker run --name redis --network minha-rede redis
docker run --network minha-rede app  # usa "redis" como hostname — DNS interno
Default bridgeUser-defined bridge
DNS por nome❌✅
IsolamentoTodos juntosIsolado por rede
InspeçãoLimitadadocker network inspect
Nunca use a default bridge em produção

Sem DNS por nome e sem isolamento real. Sempre crie redes nomeadas.

awsvpc — o modo de rede do ECS/Fargate

Cada Task ECS recebe um ENI próprio, com IP privado da VPC. Security Groups por Task, VPC Flow Logs por Task, funciona com Service Connect e Cloud Map.

JSON
{
  "networkMode": "awsvpc",
  "networkConfiguration": {
    "awsvpcConfiguration": {
      "subnets": ["subnet-xxx"],
      "securityGroups": ["sg-xxx"],
      "assignPublicIp": "DISABLED"
    }
  }
}

Service Mesh — além do networking básico

Para mTLS entre serviços, circuit breaking, rate limiting e tracing distribuído no nível de rede — entra o service mesh. No ECS: AWS App Mesh (Envoy sidecar) ou ECS Service Connect. No K8s: Istio, Linkerd, Cilium.

SHELL
Request → [Envoy sidecar] → [App container]
               ↓
         xDS control plane (App Mesh / Istio)
         (rotas, retries, timeouts, mTLS)

ECS Service Connect na prática

Service Connect vs Service Discovery (Cloud Map)

Cloud MapService Connect
DNS✅ (Route 53)✅ (localhost-like)
Load balancingClient-sideEnvoy proxy
Retry automático❌✅
Timeout config❌✅
Métricas p50/p99❌✅ CloudWatch
mTLS❌Em roadmap

Configuração de Service Connect

JSON
{
  "serviceConnectConfiguration": {
    "enabled": true,
    "namespace": "meu-cluster.local",
    "services": [{
      "portName": "http",
      "clientAliases": [{
        "port": 8080,
        "dnsName": "agente-orchestrator"
      }]
    }]
  }
}
PYTHON
# Agente A chama agente B — sem descoberta de IP manual
import httpx

# "agente-orchestrator" resolve via Envoy local
response = httpx.post(
    "http://agente-orchestrator:8080/invoke",
    json=payload,
    timeout=30.0
)
O que o Envoy faz automaticamente

Retry em 503, timeout configurável, métricas de latência p50/p99 no CloudWatch — sem nenhum código extra.

gRPC entre agentes — appProtocol crítico

JSON
{
  "portMappings": [{
    "name": "grpc",
    "containerPort": 50051,
    "protocol": "tcp",
    "appProtocol": "grpc"  // request-level LB para HTTP/2 — crítico
  }]
}
docker0veth pairsbridge awsvpcENIService Connect EnvoyVXLANgRPC

Task Definitions em profundidade

Task Definition é imutável e versionada

Cada register-task-definition cria uma nova revisão. Você nunca edita — só cria novas. Isso garante auditoria e rollback trivial.

BASH
# Rollback imediato para revisão anterior
aws ecs update-service \
  --cluster prod \
  --service agente-orchestrator \
  --task-definition agente-orchestrator:42

Anatomy completa

JSON
{
  "family": "agente-orchestrator",
  "taskRoleArn":      "arn:aws:iam::xxx:role/AgentTaskRole",
  "executionRoleArn": "arn:aws:iam::xxx:role/ECSExecutionRole",
  "networkMode": "awsvpc",
  "requiresCompatibilities": ["FARGATE"],
  "cpu": "1024", "memory": "2048",
  "containerDefinitions": [{
    "name": "orchestrator",
    "image": "xxx.dkr.ecr.region.amazonaws.com/orchestrator:sha-abc123",
    "essential": true,
    "portMappings": [{ "name": "http", "containerPort": 8080, "appProtocol": "http" }],
    "environment": [
      { "name": "ENVIRONMENT", "value": "production" }
    ],
    "secrets": [
      { "name": "ANTHROPIC_API_KEY", "valueFrom": "arn:aws:secretsmanager:...:anthropic-key" },
      { "name": "REDIS_URL",         "valueFrom": "arn:aws:secretsmanager:...:redis-url" }
    ],
    "logConfiguration": {
      "logDriver": "awslogs",
      "options": {
        "awslogs-group":         "/ecs/agente-orchestrator",
        "awslogs-region":        "us-east-1",
        "awslogs-stream-prefix": "ecs"
      }
    },
    "healthCheck": {
      "command":     ["CMD-SHELL", "curl -f http://localhost:8080/health || exit 1"],
      "interval": 30, "timeout": 5, "retries": 3, "startPeriod": 60
    },
    "stopTimeout": 120,
    "linuxParameters": { "initProcessEnabled": true }
  }]
}

taskRole vs executionRole — confusão clássica

RoleQuem usaPara quê
executionRoleECS Agent (setup)Pull ECR, buscar secrets, enviar logs para CloudWatch
taskRoleCódigo no container (runtime)S3, DynamoDB, Bedrock, SQS — assinar requests SigV4
SHELL
ECS Agent → executionRole → [pull ECR, fetch secrets] → container inicia
Container → taskRole    → [S3.getObject, sqs.sendMessage, bedrock.invoke]

stopTimeout — crítico para graceful shutdown

LangGraph Checkpointing + stopTimeout

O ECS envia SIGTERM e aguarda stopTimeout segundos antes de SIGKILL. Para agentes com checkpointing você precisa de tempo para: (1) parar de aceitar novos requests — drain do ALB leva ~30s; (2) finalizar checkpoint no Redis/DynamoDB; (3) commitar transações abertas. Defina stopTimeout maior que o deregistration_delay do target group.

Capacity Providers: Fargate vs EC2 Auto Scaling

Fargate vs FARGATE_SPOT

JSON
{
  "capacityProviderStrategy": [
    {
      "capacityProvider": "FARGATE",      // on-demand
      "weight": 1,
      "base": 2                           // mínimo 2 tasks garantidas aqui
    },
    {
      "capacityProvider": "FARGATE_SPOT", // até 70% mais barato
      "weight": 4,
      "base": 0
    }
  ]
}
NÃO use SPOT para

Agentes com estado crítico sem checkpoint, serviços com SLA rígido, e tasks com stopTimeout alto que não conseguem salvar estado em 2 min — o aviso de interrupção do SPOT é só 2 min via SIGTERM.

EC2 Auto Scaling com Managed Scaling

JSON
{
  "autoScalingGroupProvider": {
    "autoScalingGroupArn": "arn:aws:autoscaling:...",
    "managedScaling": {
      "status": "ENABLED",
      "targetCapacity": 80,         // 80% utilizado, 20% de folga para novas tasks
      "minimumScalingStepSize": 1,
      "maximumScalingStepSize": 10
    },
    "managedTerminationProtection": "ENABLED" // nunca mata instância com task rodando
  }
}

Service Auto Scaling — por CPU

BASH
aws application-autoscaling put-scaling-policy \
  --service-namespace ecs \
  --resource-id service/prod/agente-orchestrator \
  --scalable-dimension ecs:service:DesiredCount \
  --policy-type TargetTrackingScaling \
  --target-tracking-scaling-policy-configuration '{
    "TargetValue": 60.0,
    "PredefinedMetricSpecification": {
      "PredefinedMetricType": "ECSServiceAverageCPUUtilization"
    },
    "ScaleInCooldown": 300,
    "ScaleOutCooldown": 60
  }'

Service Auto Scaling — por SQS queue depth

Para agentes triggerados por filas — escala proporcionalmente ao backlog, não à CPU:

JSON
{
  "CustomizedMetricSpecification": {
    "MetricName": "ApproximateNumberOfMessagesVisible",
    "Namespace": "AWS/SQS",
    "Dimensions": [{ "Name": "QueueName", "Value": "agent-tasks" }],
    "Statistic": "Average"
  },
  "TargetValue": 10  // ~10 mensagens por task rodando
}
Task DefinitiontaskRoleexecutionRole stopTimeoutFARGATE_SPOTCapacity Provider Auto ScalingSQSgraceful shutdown

Reduzindo tamanho de imagem: do básico ao extremo

Por que tamanho importa em produção

  • Pull time: nova Task precisa baixar a imagem antes de iniciar. 1GB = minutos de cold start.
  • Superfície de ataque: cada binário desnecessário é um vetor de ataque em potencial.
  • Scan de vulnerabilidades: menos packages = menos CVEs para gerenciar.

Hierarquia de base images

ImagemTamanhoQuando usar
ubuntu:22.04~80MBRaramente em produção
python:3.12~1.0GBSó em desenvolvimento local
python:3.12-slim~130MB✅ Ideal para Python em produção
python:3.12-alpine~55MB⚠️ Cuidado com musl libc
distroless/python3~50MB✅ Produção com segurança máxima
scratch0MBSó para binários estáticos (Go)

Alpine: a armadilha do musl libc

Alpine usa musl ao invés de glibc. Muitas libs Python com extensões C (numpy, pandas, cryptography) são compiladas contra glibc. Em Alpine, você precisa compilar do zero — o que anula o ganho de tamanho.

DOCKERFILE
# ❌ Alpine + numpy = dor de cabeça
FROM python:3.12-alpine
RUN pip install numpy  # pode falhar ou demorar muito (compila C)

# ✅ slim é o melhor custo-benefício para Python
FROM python:3.12-slim
RUN pip install numpy  # wheels pré-compilados, funciona sempre

Distroless — o padrão de produção seguro

Sem shell, sem package manager, sem utilitários de sistema. Um atacante que invade o container não tem /bin/bash para trabalhar.

DOCKERFILE
FROM python:3.12-slim AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --prefix=/install -r requirements.txt
COPY src/ ./src/

FROM gcr.io/distroless/python3-debian12
COPY --from=builder /install /usr/local
COPY --from=builder /app/src /app/src
WORKDIR /app
CMD ["src/main.py"]

Técnicas de redução de camadas

DOCKERFILE
# ❌ Cada RUN cria uma camada — apt cache fica gravado
RUN apt-get update
RUN apt-get install -y curl
RUN rm -rf /var/lib/apt/lists/*

# ✅ Uma camada, cache limpo
RUN apt-get update && apt-get install -y --no-install-recommends \
    curl \
    && rm -rf /var/lib/apt/lists/*

RUN pip install --no-cache-dir -r requirements.txt

Resultado típico de otimização

EstratégiaTamanho
python:3.12 base sem cuidado~1.4GB
python:3.12-slim + multi-stage~180MB
slim + multi-stage + --no-cache-dir~140MB
distroless + multi-stage~95MB
Cold start no ECS Fargate

A diferença de 1.4GB vs 95MB pode ser 3–4 minutos a mais para uma nova Task escalar — crítico quando o Capacity Provider precisa provisionar nova capacidade.

ECR: lifecycle policies e image scanning

Lifecycle policies — evitar acumular imagens

JSON
{
  "rules": [
    {
      "rulePriority": 1,
      "description": "Manter últimas 10 imagens tagged com sha-",
      "selection": {
        "tagStatus": "tagged",
        "tagPrefixList": ["sha-"],
        "countType": "imageCountMoreThan",
        "countNumber": 10
      },
      "action": { "type": "expire" }
    },
    {
      "rulePriority": 2,
      "description": "Manter latest, main, staging",
      "selection": {
        "tagStatus": "tagged",
        "tagPrefixList": ["latest", "main", "staging"],
        "countType": "imageCountMoreThan",
        "countNumber": 3
      },
      "action": { "type": "expire" }
    },
    {
      "rulePriority": 3,
      "description": "Remover untagged após 7 dias",
      "selection": {
        "tagStatus": "untagged",
        "countType": "sinceImagePushed",
        "countUnit": "days",
        "countNumber": 7
      },
      "action": { "type": "expire" }
    }
  ]
}

Estratégia de tagging — nunca use só :latest em produção

BASH
# ❌ :latest é ambíguo — qual versão exatamente?
docker push myrepo/app:latest

# ✅ Tag imutável com SHA do commit
GIT_SHA=$(git rev-parse --short HEAD)
docker build -t myrepo/app:sha-${GIT_SHA} .
docker push myrepo/app:sha-${GIT_SHA}

# Atualiza :latest também (para referência humana)
docker tag myrepo/app:sha-${GIT_SHA} myrepo/app:latest
docker push myrepo/app:latest
Task Definition + SHA tag

Na Task Definition do ECS, sempre referencie pela tag SHA — não por :latest. Isso garante rollback determinístico: agente-orchestrator:42 sempre aponta para o mesmo binário.

ECR Image Scanning

BASH
# Scan automático no push
aws ecr put-image-scanning-configuration \
  --repository-name minha-app \
  --image-scanning-configuration scanOnPush=true

# Consultar resultados
aws ecr describe-image-scan-findings \
  --repository-name minha-app \
  --image-id imageTag=sha-abc123

Enhanced scanning (via Inspector V2): análise contínua, não só no push. Detecta CVEs em packages do OS e em libs Python/Node. Integra com EventBridge para alertas automáticos quando novas CVEs aparecem em imagens já deployadas.

ECRlifecycle policyimage scanning Inspector V2sha tagdistroless alpinemusl libccold start