Teu progresso
0 / 83 módulos0%
Estágio 03 · 03-02
BloqueadoDocker é commodity, mas a maioria dos Dockerfiles em projetos reais é desastre: imagens de 2 GB com node_modules de dev, secrets em ENV, root user, build cache não usado, layers gigantes, latência fria de minutos em deploys, vulnerabilidades não tratadas. "Docker funciona" é diferente de "Docker bem usado".
Este módulo é Docker fundo: namespaces, cgroups, OverlayFS, layer caching, multi-stage builds, BuildKit, distroless, security hardening, networking, volumes, Compose pra dev, registries. Você sai construindo imagens 50-200 MB enxutas, com builds rápidos, prontas pra Kubernetes ou plataformas serverless.
Docker é wrapper sobre features Linux:
Container = processo Linux com namespaces aplicados. Não é "VM leve", VM tem kernel próprio; container compartilha kernel do host.
Image "FROM nginx:1.27" → seu Dockerfile adiciona layers. Layers comuns ficam em cache.
FROM node:22-alpine
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN corepack enable && pnpm install --frozen-lockfile
COPY . .
RUN pnpm build
EXPOSE 3000
USER node
CMD ["node", "dist/server.js"]
Cada COPY/RUN cria layer. Ordem importa pra cache: itens que mudam menos antes (deps), código depois.
Reduz imagem final separando build e runtime:
FROM node:22-alpine AS deps
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN corepack enable && pnpm install --frozen-lockfile
FROM node:22-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN pnpm build
FROM node:22-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY package.json ./
USER node
EXPOSE 3000
CMD ["node", "dist/server.js"]
Final image só tem o necessário pra rodar. Build artifacts intermediários ficam em stages descartados.
Para Node.js mais leve: pnpm install --prod --frozen-lockfile na stage final, copiando só prod deps.
Para Next.js: output: 'standalone' em next.config.js produz .next/standalone/ com server completo + node_modules mínimas. Imagem final fica ~150 MB em vez de 1 GB.
Recomendação 2026: node:22-bookworm-slim ou gcr.io/distroless/nodejs22 na stage final.
DOCKER_BUILDKIT=1 (default em Docker 23+) habilita BuildKit:
--mount=type=cache,target=/root/.npm).--mount=type=secret,id=npm_token), não fica em layer.RUN --mount=type=ssh pra git private.Exemplo cache mount pra pnpm:
RUN --mount=type=cache,target=/pnpm/store pnpm install --frozen-lockfile
Próximo build reusa store mesmo se Dockerfile mudou.
ARM ganhou prod em 2024-2026: AWS Graviton (~20-40% cheaper que x86), Apple Silicon dev, Cloudflare/Fly.io edge ARM-only. Imagem single-arch trava deploy em provider ARM.
# Setup uma vez
docker buildx create --name multiarch --driver docker-container --bootstrap --use
# Build cross-arch + push manifest list
docker buildx build \
--platform linux/amd64,linux/arm64 \
--tag ghcr.io/me/api:v1.2.3 \
--push \
.
Resultado: registry tem manifest list apontando 2 images (uma por arch); pull do consumidor ARM puxa só a ARM. docker pull resolve automaticamente.
Performance: building ARM em runner x86 usa QEMU emulation — 5-10x mais lento. Soluções:
runs-on: ubuntu-24.04-arm (free pra public repos). Build paralelo per-arch, depois merge manifest com docker buildx imagetools create.GOARCH=arm64, copie pra image. Skip emulation.Build local usa cache local. CI sem cache backend = build do zero todo run (lento, caro).
# Registry-based cache (push/pull cache layer pra registry)
docker buildx build \
--cache-from type=registry,ref=ghcr.io/me/api:buildcache \
--cache-to type=registry,ref=ghcr.io/me/api:buildcache,mode=max \
--tag ghcr.io/me/api:latest \
--push .
# GitHub Actions cache (10GB free, expira em 7 dias sem hit)
docker buildx build \
--cache-from type=gha \
--cache-to type=gha,mode=max \
--tag ghcr.io/me/api:latest \
--push .
# S3 cache (custom, controlled retention)
docker buildx build \
--cache-from type=s3,region=us-east-1,bucket=my-buildcache \
--cache-to type=s3,region=us-east-1,bucket=my-buildcache,mode=max \
...
mode=max exporta TODOS os layer intermediários (incluindo do builder stage). mode=min exporta só o final. Use max em CI sério, min se cache budget é restrito.
# Dockerfile
RUN --mount=type=secret,id=npm_token,target=/root/.npmrc \
--mount=type=secret,id=aws_creds,target=/root/.aws/credentials \
npm install && \
aws s3 cp s3://artifacts/foo .
# Build invocation
DOCKER_BUILDKIT=1 docker build \
--secret id=npm_token,src=$HOME/.npmrc \
--secret id=aws_creds,src=$HOME/.aws/credentials \
-t myimage .
# CI (secrets vindo de env, não files)
echo -n "$NPM_TOKEN" | docker buildx build --secret id=npm_token,src=/dev/stdin ...
Secret não fica em nenhuma layer da imagem final — montado em build-time só. Diferente de ARG (que vaza em docker history) ou ENV (que persiste).
# OCI tarball local (pra inspeção, não-registry distribution)
docker buildx build --output type=oci,dest=image.tar ...
# Plain filesystem (pra extract artifacts sem container)
docker buildx build --output type=local,dest=./out ...
# Use case: build Next.js, extrair só /app/out pra servir em S3 + CloudFront.
Crucial. Sem ele, COPY . . envia node_modules, .git, build outputs pra contexto de build. Build lento e imagem inflada.
node_modules
.git
.env*
dist
.next
.cache
**/*.log
Regras:
RUN que combinam install + cleanup numa única linha (evita layer com cache lixo).ARG GIT_SHA) tarde pra não invalidar layers anteriores.COPY específico (COPY src ./src) em vez de COPY . . quando só precisa subset.Docker cria bridge docker0 por default. Container ganha veth pair, IP no range, NAT pra fora.
Drivers:
bridge (default).host (sem isolamento de rede).overlay (multi-host, Swarm/Kubernetes).macvlan (container com MAC próprio).none.Em Compose, networks default isolam por projeto. Containers no mesmo network resolvem por nome (postgres, redis).
3 tipos:
docker volume create pgdata. Persiste entre containers.-v /host/path:/container/path. Bom pra dev (live reload).Em prod: named volumes pra estado durável. Em dev: bind mounts pra código.
Cuidado: bind mount em Mac/Windows é lento (file system sync). Mac VM-based usa virtiofs/9p, performance varia.
YAML descreve serviços, networks, volumes:
services:
api:
build: .
ports: ["3000:3000"]
environment:
DATABASE_URL: postgres://app:secret@postgres:5432/app
depends_on:
postgres:
condition: service_healthy
postgres:
image: postgres:16
environment:
POSTGRES_PASSWORD: secret
volumes: [pgdata:/var/lib/postgresql/data]
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
volumes: { pgdata: }
Compose v2 (plugin) substituiu v1 (docker-compose Python). Comando: docker compose up.
depends_on apenas ordena start; não espera readiness sem condition: service_healthy.
Healthchecks importam: definem se container está "ok" pra outros dependerem.
USER node ou UID/GID explícito. Default Docker é root, péssimo.--read-only, com tmpfs pros writable paths necessários.--cap-drop=ALL --cap-add=NET_BIND_SERVICE.--security-opt=no-new-privileges.ENV API_KEY=... no Dockerfile = secret na image. Use BuildKit secrets ou runtime env.docker scout, trivy, grype. CI gate.cosign (Sigstore) pra supply chain.npm prune --production ou pnpm install --prod.apt clean && rm -rf /var/lib/apt/lists/*).node:22-alpine vs slim por compat.Meta: backend Node típico ≤ 200 MB. Front Next standalone ≤ 200 MB. Pro código novo é alcançável.
Tags:
latest em prod (mutable).@sha256:...).v1.2.3).git-sha (commit) pra rastreabilidade.Diferenças aceitáveis:
Mas: runtime base e config devem espelhar prod. Bug que aparece só em prod por divergência é cara.
Padrão: Dockerfile com stages dev e prod, ou Dockerfile + Dockerfile.dev. Compose escolhe stage via target:.
SIGTERM. Container espera stopGracePeriod (default 10s) antes de SIGKILL.tini ou dumb-init como PID 1 quando o app não reapa zombies (Node como PID 1 funciona, mas em alguns runners é safer).PID 1 reapa zumbis e propaga sinais corretamente. Linux trata PID 1 com regras especiais (defaults a ignorar SIGTERM se não há handler).
Node 12+ trata SIGTERM/SIGINT corretamente como PID 1. Mas se você fork child processes, zumbis podem acumular. tini resolve.
Em Distroless, tini já vem.
Compose: dev local, ambientes simples, prototipagem.
Kubernetes: prod escalável, multi-host, scheduler, service discovery, secrets, autoscaling. Vimos em 03-03.
Para projetos pequenos-médios em Railway/Render/Fly: Compose-like config é o que esses providers expõem por baixo.
.devcontainer/devcontainer.json define imagem + features. Reproduzível.Secrets em container é onde 80% dos breaches começam. ENV API_KEY=... em Dockerfile = secret commitado em image layer pra sempre, indexado em registries públicos. Esta seção cobre 4 vetores: (1) build-time secrets (BuildKit --mount=type=secret), (2) runtime injection (env, file, IMDS, vault), (3) K8s patterns (Secret resource, External Secrets Operator, CSI driver), (4) detecção de secret leak (gitleaks, trufflehog).
Anti-pattern primeiro — o que NÃO fazer:
# ALL TERRIBLE
ENV STRIPE_SECRET_KEY=sk_live_abc123
ARG NPM_TOKEN
RUN echo $NPM_TOKEN > /root/.npmrc
COPY .env /app/.env
RUN curl -H "Authorization: $TOKEN" https://internal/api
ENV: secret persiste em image layer; docker history revela.ARG: visível em docker inspect; também acaba em layer se referenciado em RUN.COPY .env: secret commitado em image filesystem.RUN curl ... $TOKEN: token em command line aparece em --no-cache rebuild logs e layer.Build-time secrets — BuildKit --mount=type=secret (correto):
# syntax=docker/dockerfile:1.7
FROM node:20 AS deps
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
# Secret monta em /run/secrets/<id> apenas durante RUN; não persiste em layer
RUN --mount=type=secret,id=npm_token,target=/root/.npmrc \
corepack enable && pnpm install --frozen-lockfile
FROM node:20-slim
COPY --from=deps /app/node_modules /app/node_modules
COPY . /app
WORKDIR /app
CMD ["node", "server.js"]
Build:
# Secret de file
echo "//registry.npmjs.org/:_authToken=npm_xxx" > /tmp/npmrc
docker build --secret id=npm_token,src=/tmp/npmrc -t app .
# Secret de env var (CI)
docker build --secret id=npm_token,env=NPM_TOKEN -t app .
Build-time SSH agent forwarding (pra clone de repo privado):
# syntax=docker/dockerfile:1.7
RUN --mount=type=ssh \
git clone git@github.com:myorg/private-lib.git
Build:
eval $(ssh-agent)
ssh-add ~/.ssh/id_ed25519
docker build --ssh default -t app .
Runtime secrets — 4 padrões:
1. Env vars via docker run -e (acceptable, NÃO ideal):
docker run -e DATABASE_URL=postgres://... app
docker inspect, em /proc/<pid>/environ, em logs de exception com env dump (process.env impresso).2. Env vars via --env-file:
docker run --env-file=secrets.env app
-e em runtime; só evita shell history.3. Tmpfs mount com secret file:
docker run \
--mount type=tmpfs,destination=/run/secrets,tmpfs-size=64k \
--mount type=bind,source=/host/secrets/db_password,target=/run/secrets/db_password,readonly \
app
const dbPwd = await fs.readFile('/run/secrets/db_password', 'utf8');.4. IMDS / Vault / cloud secret manager — recomendado em prod:
// App busca secret on-demand de SecretsManager (AWS) / Secret Manager (GCP)
import { SecretsManagerClient, GetSecretValueCommand } from '@aws-sdk/client-secrets-manager';
const sm = new SecretsManagerClient({ region: 'us-east-1' });
let cachedDbPwd: { value: string; fetchedAt: number } | null = null;
async function getDbPassword(): Promise<string> {
if (cachedDbPwd && Date.now() - cachedDbPwd.fetchedAt < 5 * 60 * 1000) {
return cachedDbPwd.value;
}
const resp = await sm.send(new GetSecretValueCommand({ SecretId: 'prod/db/password' }));
cachedDbPwd = { value: resp.SecretString!, fetchedAt: Date.now() };
return cachedDbPwd.value;
}
Docker Swarm secrets (legacy mas em produção):
echo "supersecret" | docker secret create db_password -
docker service create \
--name api \
--secret db_password \
--secret source=stripe_key,target=stripe_key,mode=0400 \
myorg/api:latest
/run/secrets/<name> no container.Kubernetes — Secret resource + 3 padrões avançados:
1. Secret básico (caveat: base64 NÃO é encryption):
apiVersion: v1
kind: Secret
metadata:
name: db-creds
type: Opaque
data:
password: c3VwZXJzZWNyZXQ= # base64 de 'supersecret'
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
spec:
template:
spec:
containers:
- name: api
image: myorg/api:latest
env:
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: db-creds
key: password
EncryptionConfiguration (AES-CBC ou KMS-backed).get secrets só pra service accounts que precisam.2. External Secrets Operator (ESO) — sync de Vault/AWS/GCP:
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: db-creds
spec:
refreshInterval: 1h
secretStoreRef:
name: aws-sm
kind: ClusterSecretStore
target:
name: db-creds
data:
- secretKey: password
remoteRef:
key: prod/db/password
3. CSI Secret Store Driver — mount sem K8s Secret:
apiVersion: secrets-store.csi.x-k8s.io/v1
kind: SecretProviderClass
metadata:
name: vault-db-creds
spec:
provider: vault
parameters:
vaultAddress: https://vault.internal:8200
roleName: api
objects: |
- objectName: "db_password"
secretPath: "secret/data/prod/db"
secretKey: "password"
Detecção de leaks — pre-commit + CI:
# .pre-commit-config.yaml
repos:
- repo: https://github.com/gitleaks/gitleaks
rev: v8.18.0
hooks:
- id: gitleaks
# .github/workflows/secret-scan.yml
- uses: trufflesecurity/trufflehog@main
with:
path: ./
extra_args: --only-verified
trufflehog git file://. --since-commit HEAD~100.Anti-patterns observados em produção:
ENV em Dockerfile pra "convenience": image em registro privado vaza pra terceirizado tem acesso.--build-arg: docker history revela; ARG é visível em metadata.COPY .env: literalmente coloca .env no image filesystem.process.env dump em exception handler: secrets aparecem em Sentry/Datadog.cluster-admin: comprometeu pod = comprometeu cluster.Validation — secret leak audit local:
# Gitleaks scan completo
gitleaks detect --source . --verbose --report-path leaks.json
# Auditar Dockerfile
docker history myorg/api:latest --no-trunc | grep -iE "secret|key|token|password"
# Inspect runtime container
docker inspect <container> | jq '.[0].Config.Env'
Cruza com 03-08 §2.13 (secrets management foundation), 03-08 §2.14 (supply chain — secret leak é vetor), 03-08 §2.20 (SBOM/VEX correlato), 03-02 §2.6 (BuildKit advanced — secret mount é feature dela), 03-03 §2.x (K8s production patterns).
Build pipeline em 2026 não é docker build: é docker buildx build com cache mounts (npm/Go/apt cache fora de layers), multi-arch (amd64+arm64 num único push) e distroless runtime (~5 CVEs vs ~100+ Ubuntu). Esta seção cobre BuildKit features avançadas, multi-arch via Depot.dev/buildx, distroless/Chainguard, SBOM+provenance attestations.
BuildKit fundamentals (default em Docker 23+, dockerd 23+, buildx 0.13+):
# syntax=docker/dockerfile:1.7 unlock cache/bind/secret/ssh mounts; SEM directive, BuildKit silenciosamente ignora --mount.Cache mounts — package manager + compilation cache fora de layer:
# syntax=docker/dockerfile:1.7
FROM node:22-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm \
npm ci --no-audit --no-fund
~/.npm entre builds; NÃO entra na image final (zero bloat).--mount=type=cache,id=npm,target=/root/.npm — múltiplos services compartilham o mesmo cache.Pattern Go (mod cache + build cache):
RUN --mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
go build -o /app/bin/server ./cmd/server
Pattern apt (Debian-based base):
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,target=/var/lib/apt,sharing=locked \
apt-get update && apt-get install -y --no-install-recommends curl
Bind mounts — read-only file sem COPY layer:
RUN --mount=type=bind,source=package.json,target=package.json \
--mount=type=bind,source=package-lock.json,target=package-lock.json \
--mount=type=cache,target=/root/.npm \
npm ci
Lockfile validation sem custo de layer; útil pra reproducible builds.
Multi-stage optimized — stages paralelos no BuildKit:
# syntax=docker/dockerfile:1.7
FROM node:22-alpine AS base
WORKDIR /app
FROM base AS deps
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm npm ci
FROM base AS build
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN --mount=type=cache,target=/app/.next/cache npm run build
FROM base AS prod-deps
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm npm ci --omit=dev
FROM gcr.io/distroless/nodejs22-debian12:nonroot AS runtime
WORKDIR /app
COPY --from=prod-deps /app/node_modules ./node_modules
COPY --from=build /app/.next/standalone ./
EXPOSE 3000
USER nonroot
CMD ["server.js"]
deps e prod-deps rodam em paralelo (BuildKit DAG); build depende de deps; runtime recebe só prod-deps + standalone output.
Multi-arch builds (linux/amd64 + linux/arm64):
Apple Silicon devs (arm64 local) + AWS Graviton/Ampere prod (arm64, 20-40% cheaper compute) + Cloudflare Workers + Raspberry Pi. Ship single tag, multiple architectures.
docker buildx create --name multibuilder --driver docker-container --use --bootstrap
docker buildx build --platform linux/amd64,linux/arm64 \
-t registry.example.com/logistica/api:v3.4 \
--push .
linux/arm64 runners (GA 2024+); Docker Build Cloud (native ARM); Depot.dev (managed cloud builders, native ARM, ~5min full build).FROM --platform=$BUILDPLATFORM golang:1.23 AS build
ARG TARGETOS TARGETARCH
WORKDIR /src
COPY . .
RUN --mount=type=cache,target=/go/pkg/mod \
GOOS=$TARGETOS GOARCH=$TARGETARCH go build -o /out/server ./cmd/server
FROM gcr.io/distroless/static-debian12 AS runtime
COPY --from=build /out/server /
USER nonroot
CMD ["/server"]
Distroless images (gcr.io/distroless/*, Google):
static (Go static binaries, ~2MB), base (glibc apps, ~20MB), cc (C++), java, python3, nodejs22-debian12 (~150MB), tags :nonroot e :debug.static ~2MB; nodejs22-debian12 ~150MB; Alpine node:22-alpine ~180MB; Ubuntu node:22 ~1GB.kubectl exec -it pod -- sh falha; attacker post-RCE não tem shell pra pivotar.apt-get install nmap post-compromise.:debug adiciona busybox shell): só staging; NUNCA prod.Chainguard Images (alternativa 2024+):
cgr.dev/chainguard/node:latest); Enterprise: pinned versions, SLA, FIPS variants.SBOM + provenance attestations:
docker buildx build \
--sbom=true --provenance=true \
--platform linux/amd64,linux/arm64 \
-t registry.example.com/logistica/api:v3.4 --push .
# Verificação
cosign verify-attestation registry.example.com/logistica/api:v3.4 \
--type slsaprovenance --certificate-identity-regexp '.*'
Gera SBOM (CycloneDX) + provenance (SLSA v1.0) attestations attached ao image manifest. Cruza com 03-08 §2.20 (SBOM lifecycle deep).
Logística applied stack:
/root/.npm + .next/cache); distroless nodejs22-debian12:nonroot runtime; ~150MB final image.linux/amd64 + linux/arm64 build via Depot.dev cloud builder (native ARM, ~5min full); push pra GHCR.linux/arm64 (Ampere CPUs ~30% cheaper que x86); CI auto via docker buildx build --push.Anti-patterns observados (10 itens):
docker build em vez de docker buildx build (perde cache mounts, multi-arch, attestations).RUN apt-get update && apt-get install sem --mount=type=cache,target=/var/cache/apt (re-download em todo build).node_modules na image final via COPY . (bloat 500MB+ quando só ~50MB prod-deps necessário).FROM node:22 (Debian-based ~1GB) em vez de Alpine ou distroless.COPY . . no início do Dockerfile (qualquer file change invalida cache de deps).:debug em prod (shell habilitado defeats segurança; usar :nonroot).# syntax=docker/dockerfile:1.7+ directive (cache mounts ignorados silenciosamente — build lento sem warning).root em runtime (privilege escalation vector pós-RCE; sempre :nonroot ou USER 1000).Cruza com 03-03 (K8s, image registry + imagePullPolicy), 03-04 (CI/CD, buildx em GHA matrix), 03-08 §2.20 (SBOM lifecycle + cosign), 03-05 (AWS, ECR + Graviton ARM nodes), 03-12 (Wasm, alternativa pra ultra-light deploys <10MB).
O ecossistema de containers fragmentou pós-layoffs Docker Inc 2023-2024 e re-consolidou em torno do OCI como standard de fato. Três specs convergiram em 2024: OCI image-spec 1.1 (julho 2024 — adiciona artifactType no manifest e campo subject pra referrers), OCI distribution-spec 1.1 (Referrers API), OCI runtime-spec 1.2. Isso destravou o caso de uso central da supply chain moderna: SBOM + assinatura + provenance vivem como OCI artifacts linkados à imagem via subject, no mesmo registry, sem out-of-band storage.
Em paralelo, Wolfi (undistro Linux do time Chainguard, apk-based, glibc-compatível) emergiu como evolução de distroless. Distroless (Google) ainda é válido pra Java/Go statically-linked, mas perde quando precisa de glibc + native deps (Node native modules, Python wheels com C extensions) — Wolfi resolve. Chainguard Images = vendor-curated Wolfi com SLA de zero-CVE-known + variants FIPS. Tamanhos comparados (Node 20 runtime): Wolfi ~80MB, distroless ~140MB, Alpine ~150MB, Ubuntu slim ~400MB.
OCI 1.1 introduziu artifactType no image manifest (descreve payload não-imagem: SBOM, signature, attestation) e subject (aponta pra outro manifest). O Referrers API (GET /v2/<repo>/referrers/<digest>) retorna todos manifests que apontam pra um digest. Resultado: SBOM + cosign signature + SLSA provenance ficam linkados à imagem como grafo navegável, sem tags paralelas tipo sha256-abc.sig (que era o workaround pré-1.1).
{
"schemaVersion": 2,
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"artifactType": "application/spdx+json",
"config": { "mediaType": "application/vnd.oci.empty.v1+json", "size": 2, "digest": "sha256:44136fa..." },
"layers": [{ "mediaType": "application/spdx+json", "digest": "sha256:<sbom-blob>", "size": 12345 }],
"subject": {
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"digest": "sha256:<image-digest>"
}
}
# build stage — Wolfi com toolchain
FROM cgr.dev/chainguard/node:latest-dev@sha256:<digest> AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm \
npm ci --omit=dev
COPY . .
RUN npm run build
# runtime — Wolfi minimal, sem shell, non-root por default
FROM cgr.dev/chainguard/node:latest@sha256:<digest>
WORKDIR /app
COPY --from=build --chown=nonroot:nonroot /app/dist ./dist
COPY --from=build --chown=nonroot:nonroot /app/node_modules ./node_modules
COPY --from=build --chown=nonroot:nonroot /app/package.json ./
USER nonroot
EXPOSE 3000
ENTRYPOINT ["node", "dist/server.js"]
Pin por digest (@sha256:...) é obrigatório em prod — :latest viola reproducibility. Wolfi rebuilda diariamente, então o digest muda; trate como dependency e renove via Renovate/Dependabot.
apko builda OCI images de YAML declarativo, reproducible (mesmo input → mesmo digest binário), sem Dockerfile imperativo. melange builda APKs do source de forma equivalente. Combinação: melange build gera o pacote, apko build monta a imagem.
# apko.yaml — image declarativo
contents:
repositories:
- https://packages.wolfi.dev/os
keyring:
- https://packages.wolfi.dev/os/wolfi-signing.rsa.pub
packages:
- ca-certificates-bundle
- nodejs-20
- npm
- tini
accounts:
groups:
- groupname: nonroot
gid: 65532
users:
- username: nonroot
uid: 65532
gid: 65532
run-as: 65532
entrypoint:
command: /usr/bin/tini -- /usr/bin/node /app/dist/server.js
archs:
- x86_64
- aarch64
apko build apko.yaml app:latest app.tar → imagem reproducible, OCI-compliant, multi-arch, com SBOM SPDX embedado por default.
Pipeline canônico: build → Syft gera SBOM → cosign sign image (keyless OIDC) → cosign attest SBOM (linka como referrer OCI 1.1).
# build + push com provenance + SBOM nativos
docker buildx build --platform linux/amd64,linux/arm64 \
--provenance=true --sbom=true \
-t ghcr.io/org/app:${SHA} --push .
# Syft fora do build (ou usa o SBOM auto-embedado pelo BuildKit)
syft ghcr.io/org/app:${SHA} -o spdx-json > sbom.spdx.json
# cosign sign (keyless via Fulcio + GitHub OIDC; sem key management)
COSIGN_EXPERIMENTAL=1 cosign sign ghcr.io/org/app:${SHA}
# attest SBOM (vira referrer OCI 1.1, navegável via Referrers API)
COSIGN_EXPERIMENTAL=1 cosign attest --predicate sbom.spdx.json \
--type spdx ghcr.io/org/app:${SHA}
# verify em admission controller (Kyverno/Cosign policy controller)
cosign verify ghcr.io/org/app:${SHA} \
--certificate-identity-regexp 'https://github\.com/org/.+' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com
Keyless via Fulcio (sigstore) elimina key management — assinatura é amarrada à identidade OIDC do runner CI (GitHub Actions, GitLab, Buildkite). Sem KMS, sem rotação manual, sem chaves longplayed pra vazar.
SLSA v1.0 (stable Q4 2023) define níveis de garantia da supply chain. Provenance attestation (in-toto) linka binary → source commit → builder identity. BuildKit gera nativamente via --provenance=true (modo max inclui materials completos).
# inspect provenance
cosign verify-attestation ghcr.io/org/app:${SHA} \
--type slsaprovenance \
--certificate-identity-regexp '...' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
| jq '.payload | @base64d | fromjson | .predicate'
Verificação em prod: admission controller (Kyverno, OPA Gatekeeper, Cosign policy-controller) bloqueia pods cujas imagens não tenham provenance assinada por builder confiável.
Trivy 0.55+ (Aqua, OSS) scanneia image + filesystem + IaC + K8s manifests + secrets, db atualizada de NVD + GHSA + vendor advisories. Grype (Anchore) é alternativa, integra bem com Syft (mesmo time). Docker Scout integrou ao Docker Desktop em 2024 — útil pra dev local, menos relevante em CI.
# .github/workflows/scan.yml
- name: Trivy image scan
uses: aquasecurity/trivy-action@0.28.0
with:
image-ref: ghcr.io/org/app:${{ github.sha }}
format: sarif
output: trivy.sarif
severity: CRITICAL,HIGH
exit-code: 1 # fail CI se HIGH/CRITICAL
ignore-unfixed: true # ignora CVE sem patch upstream
vuln-type: os,library
- name: Upload SARIF
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: trivy.sarif
Cruza com VEX (Vulnerability Exploitability eXchange) — declarar CVE como not_affected quando não-explorável no contexto, sem suprimir alerta global. Trivy lê VEX OpenVEX format desde 0.50.
Bake (estável desde 2023) é o substituto moderno de docker-compose build pra pipelines complexos: matrix de targets, cache export pra registry, multi-arch. HCL > YAML pra lógica condicional + variables.
# docker-bake.hcl
variable "TAG" { default = "dev" }
variable "REGISTRY" { default = "ghcr.io/org" }
group "default" {
targets = ["api", "worker"]
}
target "_common" {
platforms = ["linux/amd64", "linux/arm64"]
cache-from = ["type=registry,ref=${REGISTRY}/cache:buildcache"]
cache-to = ["type=registry,ref=${REGISTRY}/cache:buildcache,mode=max"]
attest = [
"type=provenance,mode=max",
"type=sbom"
]
}
target "api" {
inherits = ["_common"]
context = "./apps/api"
dockerfile = "Dockerfile"
tags = ["${REGISTRY}/api:${TAG}"]
args = { NODE_ENV = "production" }
}
target "worker" {
inherits = ["_common"]
context = "./apps/worker"
dockerfile = "Dockerfile"
tags = ["${REGISTRY}/worker:${TAG}"]
}
docker buildx bake --push builda os dois targets em paralelo, cada um amd64+arm64, com SBOM + provenance attestation, cache compartilhado. Cross-compile via frontend tonistiigi/xx quando precisa Go/Rust ARM em runner amd64.
Podman 5.x (Q1 2024) trouxe volume + secret handling reescritos, melhor compat com Docker API, suporte nativo a K8s YAML (podman play kube). Vantagens: daemonless (cada container = processo do user, sem privileged daemon), rootless por default, integra com systemd via quadlet (units .container declarativas). Trade-off: networking rootless via slirp4netns/pasta tem ~10-20% overhead vs bridge nativa; build via buildah (não BuildKit nativo) — features avançadas (cache mounts, secret mounts) ficaram pra trás até 2025.
# rootless run, sem daemon
podman run --rm -p 8080:80 cgr.dev/chainguard/nginx:latest
# K8s YAML direto (pod ou deployment)
podman play kube ./deploy/pod.yaml
# quadlet (systemd unit declarativa)
cat > ~/.config/containers/systemd/api.container <<EOF
[Container]
Image=ghcr.io/org/api:v1.2.3
PublishPort=3000:3000
[Service]
Restart=always
[Install]
WantedBy=default.target
EOF
systemctl --user daemon-reload && systemctl --user start api
Coexistência Podman + Docker no mesmo host gera conflitos de rede (CNI vs bridge docker0) — escolha um por host.
Base: Wolfi Node 20 (cgr.dev/chainguard/node:latest-dev build, :latest runtime, ambos pinned por digest, renovados via Renovate weekly). Build: docker buildx bake com matrix [api, worker, scheduler] × [amd64, arm64], cache export to GHCR, --provenance=max --sbom=true. CI: Trivy scan post-build, fail on HIGH/CRITICAL non-ignored, SARIF upload pro Security tab. Sign: cosign keyless via GitHub OIDC, attest SBOM (Syft-generated SPDX) + SLSA provenance como referrers OCI 1.1. Deploy: K8s admission via Kyverno verifica cosign signature + provenance issuer = token.actions.githubusercontent.com + repo regexp = org/.+. Resultado: imagem ~80MB, zero CVE HIGH known na build, supply chain auditável end-to-end.
cgr.dev/chainguard/node:latest em prod sem digest pin — rebuild diário muda imagem, quebra reproducibility.cache-from/cache-to em registry — CI cold every run, build de 5min vira 30s+ com cache quente.SOURCE_DATE_EPOCH ou base sem timestamp pinned — drift entre builds, "reproducible" só no nome.§2.5 (distroless intro — Wolfi é evolução natural), §2.6 (BuildKit foundation — Bake é frontend dele), §2.21 (BuildKit cache mounts + multi-arch), §2.20 (Docker secrets — cosign keyless é mesma família de "no long-lived creds"), 03-08 §2.14 (supply chain security policies), 03-08 §2.20 (SBOM lifecycle + VEX), 03-04 (CI gate scanning + matrix bake), 03-03 (K8s admission controllers — Kyverno verifica cosign attestations), 04-08 §2.22 (edge runtimes — alternativa quando overhead de container é demais).
Você precisa, sem consultar:
tini.latest vs SHA-pinning.Containerizar Logística v1 (apprentice) com produção em mente.
node:22-bookworm-slim.output: 'standalone'.api espera saudável.pgdata e redisdata..env.example documentando vars.docker-compose.test.yml) com profile separado.target: builder em vez de runner.USER non-root em todas imagens.--read-only no runtime quando viável (com tmpfs em /tmp).cap_drop: [ALL].security_opt: [no-new-privileges:true].trivy image ou docker scout cves em ambas imagens..dockerignore completo.SIGTERM corretamente. docker stop graceful.FROM node:22 (sem tag específica de OS).RUN apt-get install sem cleanup na mesma layer.COPY . . sem .dockerignore.latest tag em produção.docker images).kubectl debug ou ephemeral container).docker buildx cross-platform (linux/amd64 + linux/arm64).Threshold de Maestria
Acerte todas as 5 pra marcar o módulo como concluído. Sem pressa, sem timer. Tudo fica salvo no teu navegador.
Q1Qual a diferença fundamental entre namespaces e cgroups no Linux do ponto de vista de containers?
Q2Por que `ENV API_KEY=...` em Dockerfile é antipadrão crítico?
Q3Qual a vantagem do BuildKit `--mount=type=cache,target=/root/.npm` em build CI?
Q4Por que escolher distroless ou Wolfi como runtime em vez de Ubuntu/Debian completo?
Q5O que `cosign sign` keyless via OIDC resolve que assinatura com chave estática (KMS) NÃO resolve?
Destrava
03-02 é prereq dos seguintes módulos: