Aula 01 · Build & Ship

Tudo começa na imagem

Do conceito à prática

Antes de rodar, escalar ou orquestrar, existe uma imagem. Nesta aula vamos do container como processo isolado aos padrões da OCI e aos runtimes, passando pela anatomia de uma imagem e pelo Dockerfile enxuto e seguro, até versionar e publicar no Docker Hub.

11 tópicos7 exercíciosOCI · runtimes · Dockerfile · Hadolint · Trivy · Docker HubGitHub Codespaces
Roteiro da aula

Da ideia à imagem publicada

EntenderTópicos 1.1–1.4 · bare metal, VM e container, por que containers, OCI, runtimes, anatomia da imagemExercício 1.2
→
ConstruirTópicos 1.5–1.7 · primeira imagem, anatomia e camadas, boas práticas e HadolintExercícios 1.1, 1.3–1.4
→
OtimizarTópicos 1.8–1.9 · medir tamanho, CVEs e SBOM (cadeia de suprimentos), resolver com multi-stage e distrolessExercícios 1.5–1.6
→
DistribuirTópico 1.10 · tags, SemVer, push e multi-plataformaExercício 1.7

Ao final da aula você vai saber

  • Explicar o que é um container e quem o executa (runtimes)
  • Ler a estrutura de uma imagem OCI: index, manifest, config e camadas
  • Escrever um Dockerfile otimizado, validado pelo Hadolint
  • Reduzir tamanho e vulnerabilidades com multi-stage e distroless
  • Versionar e publicar imagens com uma estratégia de tags

Por que isso importa

  • Escalabilidade: imagem menor, pull e cold start mais rápidos
  • FinOps: menos transferência e armazenamento
  • Segurança: menos pacotes, menos CVEs
  • Tudo o que vem depois (containers, Kubernetes, EKS) roda a partir dessa imagem
Tópicos
    1.1

    Onde tudo começou

    Conceito

    Três gerações de infraestrutura

    Servidor físicoMáquina virtualContainer
    IsolaHardware inteiroHardware virtual + SO convidadoProcesso (kernel compartilhado)
    InicializaçãoMinutosDezenas de segundosMilissegundos a segundos
    TamanhoDisponível no hardwareGB (SO completo)MB (só app + dependências)
    Densidade1 aplicação por máquinaDezenas por hostCentenas por host
    EntregaInstalação manualImagem de VM (AMI)Imagem OCI versionada em registry

    Do hardware até a aplicação

    Bare metal
    App A
    App B
    Bins / Libs compartilhadas
    Sistema operacional
    Hardware

    Uma máquina, um SO. Apps disputam as mesmas bibliotecas e recursos.

    Máquina virtual
    App A
    Bins / Libs
    SO convidado
    VM
    App B
    Bins / Libs
    SO convidado
    VM
    Hypervisor
    SO do host (ou hypervisor tipo 1)
    Hardware

    Cada VM carrega um SO completo com kernel próprio. Isolamento forte, custo em GB e boot lento.

    Container
    App A
    Bins / Libs
    container
    App B
    Bins / Libs
    container
    Container runtime (containerd + runc)
    SO do host kernel compartilhado
    Hardware (em cloud, uma VM)

    Cada container leva só a app e suas dependências. O kernel é do host: leve, rápido e denso.

    Por que precisamos de containers?

    “Na minha máquina funciona”

    Versões diferentes de linguagem, bibliotecas e SO entre o notebook do dev, homologação e produção.

    Conflito de dependências

    Duas aplicações no mesmo servidor precisando de versões diferentes da mesma biblioteca.

    Ambientes que divergem

    Servidores configurados à mão acumulam ajustes (drift) e ninguém consegue reproduzi-los.

    Entrega lenta

    Provisionar uma VM levava dias de chamado; o deploy era manual, raro e arriscado.

    Recurso ocioso

    Um servidor por aplicação, com boa parte da CPU e da memória sem uso.

    A resposta

    Empacotar a aplicação com tudo o que ela precisa num artefato único, leve e que roda igual em qualquer lugar.

    Por que foi adotado em escala

    • Experiência simples: o Docker (2013) juntou tecnologias que já existiam no kernel (namespaces, cgroups) numa experiência de três comandos: build, push, run.
    • Artefato imutável e portátil: a mesma imagem roda no notebook, no CI, no data center e em qualquer cloud.
    • Leve: sobe em segundos e ocupa MB, então dá para rodar muitas réplicas por servidor e criar ou destruir sob demanda.
    • Distribuição fácil: registries como o Docker Hub tornaram o compartilhamento de software tão simples quanto um git pull.
    • Padrão aberto: OCI (2015) e CNCF garantiram que ninguém ficasse preso a um fornecedor, e o ecossistema cresceu em torno do mesmo formato.
    • Encaixe perfeito com cloud e microsserviços: cada serviço vira um container independente, escalado e atualizado separadamente. O Kubernetes (2014) virou o padrão para orquestrar isso.

    O que os containers habilitaram no DevOps

    AntesCom containers
    EntregaPacote gerado a cada ambiente, deploy manualBuild once, deploy many: a mesma imagem passa por todos os ambientes no CI/CD
    AmbienteDocumento de instalação e servidor configurado à mãoDockerfile versionado no Git: ambiente como código
    RollbackReinstalar a versão anterior e torcerVoltar para a tag anterior em segundos
    TestesAmbiente de teste compartilhado e disputadoAmbientes efêmeros por branch ou pull request, com dependências reais
    TimesDeploy do monolito coordenado entre áreasMicrosserviços com ciclos de entrega independentes
    OperaçãoEscala e recuperação manuaisAutoescala e auto-reparo com orquestradores; GitOps e Platform Engineering sobre a mesma base

    Container não é DevOps, mas foi o artefato comum que permitiu automatizar o caminho do commit até a produção.

    1.2

    Open Container Initiative (OCI)

    Conceito

    Um padrão aberto para containers

    Projeto da Linux Foundation criado em 2015 para garantir interoperabilidade: uma imagem criada com uma ferramenta roda em qualquer outra (Docker, Podman, containerd, CRI-O, Kubernetes, ECS, EKS).

    image-spec

    Formato da imagem: index, manifest, config e camadas, endereçados por digest SHA-256.

    github.com/opencontainers/image-spec

    runtime-spec

    Como executar um container a partir de um bundle (rootfs + config.json). Referência: runc.

    github.com/opencontainers/runtime-spec

    distribution-spec

    API HTTP de push e pull usada pelos registries (Docker Hub, ECR, GHCR, Harbor).

    github.com/opencontainers/distribution-spec

    Repositório oficial da OCI, com as três especificações e o runc: github.com/opencontainers · site: opencontainers.org

    O que a padronização trouxe

    • Portabilidade: build com Docker, execução com containerd no Kubernetes, sem conversão.
    • Liberdade de escolha: troca de ferramenta ou fornecedor sem refazer as imagens.
    • Imagens multi-plataforma: um único nome serve amd64 e arm64 (Graviton, Apple Silicon).
    • Ecossistema: scanners, assinaturas, SBOM e registries falam o mesmo formato.

    Participam da OCI empresas como Docker, Google, Microsoft, AWS, Red Hat e IBM.

    1.3

    Container runtimes

    Conceito

    O que é um container runtime?

    • Container runtime é o componente responsável por criar, iniciar e gerenciar containers no sistema operacional.
    • Executa containers de acordo com padrões, como a especificação da OCI.
    • Basicamente, pega uma imagem de container e a transforma em um processo vivo, isolado no sistema.

    Tipos de container runtime

    TipoDescriçãoExemplos
    Low-level
    baixo nível
    Só executam o container: criam, isolam e gerenciam o processo. Simples e diretos.runc, crun, runsc (gVisor)
    High-level
    alto nível
    Gerenciam o ciclo de vida completo (baixar imagem, criar, iniciar, parar, remover) e chamam um low-level para executar.containerd, CRI-O, Docker Engine
    Sandboxed
    segurança extra
    Executam containers dentro de máquinas virtuais leves para aumentar o isolamento.Kata Containers, gVisor, Firecracker

    Cada camada, um papel

    Low-level

    • Transformam uma imagem em um processo no Linux
    • Trabalham direto com cgroups e namespaces
    • Não gerenciam imagens nem clusters
    • runc: referência da OCI · crun: em C, mais leve · runsc: intercepta chamadas de sistema

    High-level

    • Baixam imagens, criam snapshots, montam redes e volumes
    • Chamam um low-level (runc) para a execução final
    • containerd: usado pelo Docker e pelo Kubernetes (CRI) · CRI-O: feito para o Kubernetes · Docker Engine: CLI + API + containerd

    Sandboxed

    • Em vez de isolar só no kernel, criam uma pequena VM por container
    • Para multi-tenant, cloud pública e setores regulados
    • Kata: microVM por container · gVisor: kernel em user-space · Firecracker: base do AWS Lambda e Fargate

    Docker e o kernel Linux

    Docker Engine
    docker CLI
    ↓ REST API
    dockerd imagens, redes, volumes, build
    ↓ gRPC
    containerd runtime de alto nível
    ↓
    containerd-shim-runc-v2 um por container
    ↓
    runc runtime de baixo nível · usa a libcontainer
    ↓ chama o kernel
    • docker CLI: cliente. Só envia requisições para a API.
    • dockerd: o daemon. Gerencia imagens, redes, volumes e build.
    • containerd: runtime de alto nível. Baixa imagens, cria snapshots e controla o ciclo de vida.
    • shim: mantém cada container vivo mesmo se o dockerd ou o containerd reiniciarem.
    • runc: runtime de baixo nível. Configura namespaces e cgroups e inicia o processo.

    A libcontainer nasceu no Docker 0.9 (2014) substituindo o LXC, foi doada à OCI em 2015 e virou o runc. Desde o Docker 1.11 (2016) o Engine roda sobre containerd + runc.

    Linux kernel os recursos por trás do isolamento
    cgroupsControla e limita CPU, memória, I/O e número de processos de cada container
    namespacesCada container tem seus próprios processos, rede, pontos de montagem e hostname
    capabilitiesDivide os poderes do root em partes; o container recebe só as necessárias
    seccompFiltra as chamadas de sistema que o processo pode fazer
    SELinux / AppArmorPolíticas obrigatórias que restringem o que o processo pode acessar
    NetfilterFirewall, NAT e filtragem do tráfego dos containers (iptables/nftables)
    NetlinkInterface entre kernel e user-space para configurar interfaces e rotas
    OverlayFSUnion filesystem que empilha as camadas da imagem e a camada de escrita

    O Kubernetes usa o mesmo caminho a partir do containerd (via CRI), sem o dockerd.

    1.4

    Anatomia de uma imagem: Docker × OCI

    Conceito

    Por que o padrão OCI surgiu?

    O formato de imagem nasceu dentro do Docker e, até 2015, era definido e controlado por uma única empresa. Com concorrentes surgindo, o risco era cada ferramenta ter o seu formato.

    mar/2013Docker é lançado com formato próprio: camadas identificadas por IDs aleatórios, sem hash de conteúdo
    dez/2014CoreOS lança o rkt e a especificação appc (App Container), com outro formato de imagem (ACI)
    22/jun/2015Nasce na DockerCon, sob a Linux Foundation, o Open Container Project (depois OCI). Docker doa o formato e o runtime (libcontainer → runc). Fundadores incluem Docker, CoreOS, Google, Microsoft, Red Hat, AWS e IBM
    2016Docker 1.10 (fev) adota o Image Manifest V2, Schema 2, endereçado por conteúdo. Em abril a OCI cria a image-spec a partir do Docker v2.2. No fim do ano o appc para de evoluir
    19/jul/2017runtime-spec e image-spec 1.0
    4/mai/2021distribution-spec 1.0, baseada na Docker Registry HTTP API V2. Em mar/2024 saem as versões 1.1 de image e distribution

    O problema

    Sem padrão, cada ferramenta teria seu formato: uma imagem feita no Docker poderia não rodar no rkt, no Kubernetes ou numa cloud. O mercado ficaria preso a um fornecedor.

    A solução

    Em vez de criar algo do zero, a OCI padronizou o que já funcionava: o formato do Docker, com governança neutra e aberta para todo o ecossistema.

    Resultado

    Docker, Podman, containerd, CRI-O, Kubernetes, ECS e EKS falam o mesmo formato. Por isso uma imagem Docker e uma imagem OCI são quase idênticas.

    Fontes: anúncio da OCI (2015) · CoreOS Rocket (2014) · proposta da image-spec · appc · OCI 1.0 (2017) · distribution-spec 1.0 (2021)

    Os blocos de uma imagem: Docker × OCI

    Imagem Docker

    Formato criado pelo Docker e especificado como Image Manifest V2, Schema 2 (Docker 1.10, 2016).

    Manifest listmulti-arquitetura
    ↓
    Manifest V2schema 2
    ↓
    ConfigJSON do container
    Layerstar + gzip
    Origem
    Docker Inc., formato proprietário que virou padrão de fato
    Em disco
    manifest.json + repositories (docker save legado)
    Uso hoje
    Imagens antigas e registries legados; ainda aceito por todos os runtimes
    Imagem OCI

    Padrão aberto da Linux Foundation: OCI image-spec (v1.0 em 2017, v1.1 em 2024), derivado do formato Docker.

    Image indexmulti-arquitetura
    ↓
    Image manifestv1
    ↓
    Configmesmo modelo
    Layerstar + gzip ou zstd
    Origem
    Comunidade OCI (Docker, Google, Red Hat, AWS, Microsoft…)
    Em disco
    oci-layout + index.json + blobs/sha256/
    Extras
    Annotations e artefatos ligados à imagem (SBOM, assinaturas) via subject/referrers
    Uso hoje
    Padrão atual: containerd, Podman, Kubernetes, BuildKit e as imagens oficiais do Docker Hub

    Na prática são quase idênticos: mesma ideia de manifest, config e camadas, endereçados por digest. Mudam os media types e os recursos extras do OCI. Todo runtime atual lê os dois, e o Docker converte entre eles de forma transparente.

    Tag × digest: endereçamento por conteúdo

    Anotação · endereçamento por conteúdo

    Cada bloco da imagem (index, manifest, config e cada camada) é identificado pelo hash SHA-256 do próprio conteúdo: o digest (sha256:3f9c…). Mesmo conteúdo gera sempre o mesmo digest; mudou um único byte, o digest é outro.

    • Integridade: no pull, o Docker recalcula o hash de cada blob e rejeita o que não bater.
    • Reaproveitamento: camadas com o mesmo digest são baixadas e armazenadas uma única vez, mesmo usadas por várias imagens.
    • Tag = ponteiro móvel: nginx:stable-alpine hoje aponta para um digest e, depois de uma atualização, para outro. Funciona como um branch no Git.
    • Digest = identidade imutável: nginx@sha256:… sempre traz exatamente os mesmos bytes. Funciona como o hash de um commit.

    Por isso, em produção, fixe a imagem pelo digest (ou use imagem:tag@sha256:…): garante que o que foi testado é o que está rodando.

    Recomendado: versão + SHA curto do commit
    v1.0.0-3123a51
    v1.0.0 versão no formato vMAJOR.MINOR.PATCH (SemVerref): diz o que mudou
    3123a51 os 7 primeiros caracteres do SHA do commit (git rev-parse --short HEAD): diz de onde veio
    • Rastreável: git show 3123a51 mostra o commit, o autor e a mudança que gerou a imagem.
    • Única e imutável: cada commit gera uma tag diferente, que nunca é reaproveitada. Com tag immutability ativo no registry, nem por engano ela é sobrescrita.
    • Simples: legível para pessoas, sem precisar copiar o digest completo.

    Em SemVer, o trecho após o hífen é um identificador de pré-release; como tags Docker não aceitam + (build metadata), o hífen é a convenção usada.

    Single-platform × multi-platform

    Single-platform
    app:1.0 tag
    ↓
    Manifest
    ↓
    Config linux/amd64
    Layers

    Um manifest, uma config, um conjunto de camadas. Só roda na arquitetura em que foi construída.

    Multi-platform
    app:1.0 tag
    ↓
    Index manifest list
    ↓
    Manifest linux/amd64
    ↓
    Config
    Layers
    ↓
    Manifest linux/arm64
    ↓
    Config
    Layers

    Um índice aponta para um manifest por plataforma, cada um com sua config e suas camadas.

    Como o Docker escolhe

    No pull/run, seleciona automaticamente a variante da arquitetura do host: arm64 num Mac M-series ou Graviton, amd64 num x86. Para forçar: --platform linux/arm64.

    Como construir

    • Emulação QEMU: mais simples, sem mudar o Dockerfile, porém mais lenta
    • Nós nativos por arquitetura no builder: mais rápido
    • Cross-compilation com multi-stage (Go, Rust)

    Fonte: Docker docs · Multi-platform builds. Na prática: exercício 1.2, parte 3, e exercício 1.7, parte 2.

    Como emular outra arquitetura localmente?

    O host do Codespaces é amd64. Sem ajuda, o kernel dele não sabe executar um binário compilado para arm64 — e vice-versa num Mac Apple Silicon. É aí que entra o QEMU.

    O que é o QEMU

    Emulador de processador de código aberto, capaz de traduzir instruções de uma arquitetura para outra em tempo real (por exemplo, arm64 → amd64).

    binfmt_misc: a ponte com o kernel

    O Docker registra o QEMU no kernel do host via binfmt_misc. A partir daí, sempre que o kernel encontra um binário de outra arquitetura, ele encaminha a execução ao QEMU em vez de recusar — de forma transparente para o container e para quem roda o docker run.

    Emulado é compatível, não rápido: instrução por instrução traduzida custa tempo de CPU, então serve para testar e validar --platform, não para produção. Mãos à obra no exercício 1.2, parte 3.

    Formato Docker × formato OCI

    Docker (Image Manifest V2, Schema 2)OCI (image-spec v1)
    Lista multi-plataformaManifest list
    …docker.distribution.manifest.list.v2+json
    Image index
    …oci.image.index.v1+json
    Manifest…docker.distribution.manifest.v2+json…oci.image.manifest.v1+json
    Camadas…docker.image.rootfs.diff.tar.gzip…oci.image.layer.v1.tar+gzip (também zstd)
    Em discomanifest.json + repositoriesoci-layout + index.json + blobs/sha256/
    Extras—Annotations, artefatos (SBOM, assinaturas) ligados à imagem

    O formato OCI nasceu do formato Docker, por isso são quase idênticos. Registries e runtimes atuais aceitam os dois.

    Prática
    Exercício 1.2

    Formato Docker × formato OCI na prática

    ~30 min

    Usar docker manifest inspect para enxergar, num registry real, exatamente as peças da tabela acima: o índice multi-plataforma, o manifest de cada plataforma e as camadas. Depois, olhar por dentro da sua própria imagem para ver o mesmo formato Docker gravado em disco, e rodar localmente, via QEMU, cada uma das plataformas do índice inspecionado.

    Parte 1 · O índice multi-plataforma

    1. Peça o manifesto de uma imagem multi-plataforma do Docker Hub. Sem --verbose, o Docker resolve para a plataforma do seu host e mostra só aquele manifest:

      terminal
      docker manifest inspect nginx:stable-alpine

      Repare no mediaType do manifest (…docker.distribution.manifest.v2+json) e na lista layers, cada uma com seu próprio mediaType (…docker.image.rootfs.diff.tar.gzip) e digest.

    2. Agora peça o manifesto sem resolver a plataforma: o --verbose mostra o índice completo, com um manifest por arquitetura:

      terminal
      docker manifest inspect --verbose nginx:stable-alpine

      O conjunto dessas entradas, uma por Platform.architecture, é a manifest list — o equivalente Docker do image index da OCI. Conte quantas plataformas aparecem.

    Parte 2 · Sua imagem, o mesmo formato, gravado em disco

    1. A imagem do exercício 1.1 ainda não foi para um registry, então docker manifest inspect não a alcança — ele só lê de registries. Para ver o formato Docker local, exporte-a com docker save:

      terminal
      docker save "$IMAGE:1.0.0" -o /tmp/site.tar

      Agora liste o conteúdo do tar:

      terminal
      tar -tvf /tmp/site.tar

      Aparecem um manifest.json, um repositories e um arquivo por camada — exatamente a linha "Em disco" da tabela para o formato Docker, sem oci-layout nem index.json.

    2. Abra o manifest.json e confira que ele referencia o Config e a lista de Layers da sua imagem, descritos no mesmo vocabulário do formato Docker que você viu na Parte 1:

      terminal
      tar -xOf /tmp/site.tar manifest.json | python3 -m json.tool

    Parte 3 · Rodando as duas plataformas do índice, via QEMU

    1. Registre os emuladores QEMU no host (uma vez por Codespace):

      terminal
      docker run --privileged --rm tonistiigi/binfmt --install all
    2. Emular a imagem de uma arquitetura ARM64 — o manifest linux/arm64 do índice, traduzido instrução a instrução pelo QEMU:

      terminal
      docker run --rm --platform linux/arm64 alpine:3.22 uname -m   # → aarch64, emulado
    3. Emular a imagem de uma arquitetura AMD64 — o manifest linux/amd64 do mesmo índice, nativo na arquitetura do Codespace:

      terminal
      docker run --rm --platform linux/amd64 alpine:3.22 uname -m   # → x86_64, nativo
    Resultado esperado
    • Sem --verbose: um manifest só, com mediaType docker.distribution.manifest.v2+json e uma lista de layers.
    • Com --verbose: várias entradas, uma por Platform.architecture (ao menos amd64 e arm64), cada uma com seu próprio manifest e digest — a manifest list completa.
    • tar -tvf lista manifest.json, repositories e um .tar por camada.
    • --platform linux/amd64 devolve x86_64 sem aviso; --platform linux/arm64 devolve aarch64, ambos a partir do mesmo índice inspecionado na Parte 1.
    O que aprendemos com este exercício?
    • Uma imagem é um conjunto de documentos: índice multi-plataforma → manifest de cada plataforma → config e camadas, todos endereçados por digest.
    • O mesmo nome de imagem entrega binários diferentes conforme a arquitetura: o cliente escolhe o manifest certo.
    • Os formatos Docker e OCI têm a mesma estrutura; o que muda é o mediaType.
    • Com o QEMU, dá para rodar uma imagem de outra arquitetura para testar.
    Por que não dá pra inspecionar a imagem local direto

    docker manifest inspect conversa com a distribution-spec de um registry; uma imagem que só existe no cache local (docker image ls) ainda não tem manifest publicado. É por isso que, no exercício 1.7, depois do docker push, você vai poder rodar docker manifest inspect "$IMAGE:1.0.0" nela também.

    1.5

    Dockerfile → imagem → registry → container

    Conceito

    O ciclo de vida de uma imagem

    1Dockerfileescrever
    FROM nginx:stable-alpine
    COPY index.html /usr/share/…
    CMD ["nginx","-g","daemon off;"]

    A receita em texto: imagem base, arquivos, comandos de build e como iniciar o processo.

    Onde viveRepositório Git, versionado junto com o código
    Tópicos 1.5–1.7
    →
    2Imagemconstruir
    docker build -t user/site:1.0.0 .

    O BuildKit executa cada instrução, gera camadas somente leitura e a config. O resultado ganha um digest sha256.

    Onde viveCache local do Docker (docker image ls)
    Tópicos 1.4 e 1.6
    →
    3Registrydistribuir
    docker push user/site:1.0.0

    Envia só as camadas que o registry ainda não tem (distribution-spec). Outros ambientes fazem pull pela tag ou pelo digest.

    Onde viveDocker Hub, Amazon ECR, GHCR, Harbor
    Tópico 1.10
    →
    4Containerexecutar
    docker run -d -p 8080:80 user/site:1.0.0

    O runtime baixa a imagem se preciso, adiciona uma camada de escrita, aplica namespaces e cgroups e inicia o processo como PID 1.

    Onde viveHost ou nó do cluster, enquanto estiver rodando
    Aula 02
    Build once, run anywhere: o mesmo artefato, com o mesmo digest, sai do build e roda no notebook, no CI, em homologação e em produção. Muda a configuração do ambiente (variáveis, secrets), nunca a imagem.

    Instruções essenciais na construção de um Dockerfile

    InstruçãoO que fazObservação
    ADDAdiciona arquivos e diretórios locais ou remotosTambém extrai tar e baixa URL; prefira COPY
    ARGUsa variáveis de tempo de buildNunca para segredos
    CMDEspecifica o comando padrãoSubstituído pelos argumentos do docker run
    COPYCopia arquivos e diretóriosAceita --from para multi-stage
    ENTRYPOINTEspecifica o executável padrãoUse a forma exec: ["app"]
    ENVDefine variáveis de ambientePersistem na imagem e no container
    EXPOSEDescreve em quais portas a aplicação escutaSó documenta; não publica a porta
    FROMCria um novo estágio de build a partir de uma imagem baseFixe a versão, nunca latest
    HEALTHCHECKVerifica a saúde do containerUsado pelo Docker e pelo Swarm
    InstruçãoO que fazObservação
    LABELAdiciona metadados à imagemUse org.opencontainers.image.*
    MAINTAINEREspecifica o autor da imagemObsoleto: use LABEL
    ONBUILDInstruções executadas quando a imagem for usada como base em outro buildPouco usado
    RUNExecuta comandos de buildCada RUN gera uma camada
    SHELLDefine o shell padrão da imagemPadrão: ["/bin/sh", "-c"]
    STOPSIGNALDefine o sinal enviado para encerrar o containerPadrão: SIGTERM
    USERDefine o usuário e o grupoRode como não-root
    VOLUMECria pontos de montagem de volumeDados fora da camada de escrita
    WORKDIRMuda o diretório de trabalhoPrefira a cd dentro de RUN

    As 18 instruções da referência oficial do Dockerfile, na mesma ordem da documentação.

    ENTRYPOINT × CMD e o contexto de build

    Dockerfiledocker run imgdocker run img --help
    CMD ["nginx","-g","daemon off;"]nginx -g daemon off;--help (substitui o CMD)
    ENTRYPOINT ["nginx"]
    CMD ["-g","daemon off;"]
    nginx -g daemon off;nginx --help
    • docker build -t usuario/app:1.0.0 .: o . é o contexto, tudo que o build pode copiar.
    • Outro nome de arquivo: docker build -f Dockerfile.dev -t app:dev .
    • O .dockerignore tira do contexto .git, node_modules, segredos e artefatos locais.
    Prática
    Exercício 1.1

    Preparando o ambiente e criando a primeira imagem

    ~30 min

    Validar o Docker no Codespaces, escrever um Dockerfile, construir a imagem e testá-la. A imagem criada aqui é a base dos próximos exercícios.

    Parte 1 · Preparando o ambiente

    1. Confira o cliente e o servidor Docker:

      terminal
      docker version

      Rode o primeiro container:

      terminal
      docker run --rm hello-world
    2. Compare o kernel e a distribuição do host (tópico 1.1 na prática):

      terminal
      uname -r; grep PRETTY_NAME /etc/os-release

      Agora os mesmos dados, vistos de dentro de um container Alpine:

      terminal
      docker run --rm alpine:3.22 sh -c 'uname -r; grep PRETTY_NAME /etc/os-release'

    Parte 2 · Criando a imagem

    1. Crie a pasta do laboratório e entre nela:

      terminal
      mkdir -p ~/labs/site && cd ~/labs/site

      Crie a página:

      terminal
      cat > index.html <<'EOF'
      <!doctype html>
      <html lang="pt-BR">
      <head><meta charset="utf-8"><title>Container Technologies | Lab</title></head>
      <body>
        <h1>Container Technologies</h1>
        <p>Minha primeira imagem Docker no GitHub Codespaces.</p>
        <p>Versão da aplicação: 1.0.0</p>
      </body>
      </html>
      EOF
    2. Crie o Dockerfile:

      terminal
      cat > Dockerfile <<'EOF'
      FROM nginx:stable-alpine
      LABEL org.opencontainers.image.title="site" \
            org.opencontainers.image.description="Página servida pelo NGINX"
      COPY index.html /usr/share/nginx/html/index.html
      EXPOSE 80
      HEALTHCHECK --interval=10s --timeout=3s CMD wget -qO- http://localhost/ || exit 1
      CMD ["nginx", "-g", "daemon off;"]
      EOF

      Confira o arquivo gerado:

      terminal
      cat Dockerfile
    3. Crie o .dockerignore para tirar do contexto de build o que não deve ir para a imagem:

      terminal
      cat > .dockerignore <<'EOF'
      .git
      *.md
      .env
      EOF

      Confira o arquivo gerado:

      terminal
      cat .dockerignore
    4. Defina o usuário do Docker Hub e o nome da imagem (variáveis usadas até o exercício 1.7):

      terminal
      export DOCKERHUB_USER="seu-usuario-dockerhub"
      export IMAGE="$DOCKERHUB_USER/container-technologies"

      Construa a imagem:

      terminal
      docker build -t "$IMAGE:1.0.0" .

      Confira que ela aparece no cache local:

      terminal
      docker image ls "$IMAGE"
    5. Execute um container a partir da imagem criada. -d roda em segundo plano, --name dá um nome e -p 8080:80 liga a porta 8080 do Codespaces à porta 80 do container:

      terminal
      docker run -d --name site -p 8080:80 "$IMAGE:1.0.0"
    6. Liste os containers em execução. Aguarde alguns segundos até o STATUS mostrar (healthy), resultado do HEALTHCHECK:

      terminal
      docker ps
    7. Acesse a aplicação pelo terminal e, depois, pelo navegador na aba Ports (porta 8080):

      terminal
      curl localhost:8080
    8. Pare e remova o container em execução:

      terminal
      docker rm -f site

      Confirme que ele sumiu da lista:

      terminal
      docker ps -a --filter name=site
    Resultado esperado
    • docker version mostra Client e Server; o uname -r é igual no host e no container, mas o os-release muda (Ubuntu × Alpine).
    • A imagem aparece com a tag 1.0.0; o docker ps mostra o container site com 0.0.0.0:8080->80/tcp e (healthy).
    • O curl devolve o HTML da página, que também abre em https://…-8080.app.github.dev; depois do docker rm -f, o container some da lista.
    O que aprendemos com este exercício?
    • Um container compartilha o kernel do host: o uname -r é o mesmo, mas a distribuição (os-release) é a da imagem.
    • O ciclo básico: Dockerfile → docker build → imagem com tag → docker run → container.
    • O .dockerignore enxuga o contexto de build, e o HEALTHCHECK deixa o Docker saber se a aplicação está saudável.
    • Publicar a porta (-p 8080:80) é o que liga o container ao mundo de fora.
    Mantenha o terminal aberto

    As variáveis DOCKERHUB_USER e IMAGE são usadas até o exercício 1.7. Se abrir outro terminal, exporte-as de novo.

    1.6

    Camadas e cache de build

    Conceito

    O sistema de arquivos por trás das camadas

    Union filesystem (OverlayFS)
    merged o que o container enxerga: uma árvore única
    ↑ sobreposição
    upperdir camada de escrita do container · copy-on-write
    lowerdir 3 COPY index.html · somente leitura
    lowerdir 2 RUN apk add … · somente leitura
    lowerdir 1 FROM alpine · somente leitura
    • O kernel empilha as camadas da imagem (lowerdir) e uma camada gravável (upperdir) e mostra ao container uma visão única (merged).
    • Copy-on-write: ao alterar um arquivo de uma camada inferior, ele é copiado para a upperdir antes da escrita. A imagem nunca muda.
    • Apagar um arquivo cria um whiteout na camada de cima: ele some da visão, mas continua ocupando espaço na camada de baixo.
    • Várias imagens e containers compartilham as mesmas camadas em disco.
    Backend de armazenamentoSituação
    containerd image store (snapshotter overlayfs)Padrão em instalações novas desde o Docker Engine 29 (nov/2025). É o mesmo mecanismo do containerd no Kubernetes e suporta imagens multi-plataforma e attestations nativamente
    overlay2Driver clássico, padrão por anos e ainda o mais compatível. Continua em uso em instalações atualizadas de versões anteriores
    fuse-overlayfs, btrfs, zfs, vfsCasos específicos: rootless em kernel antigo, sistemas de arquivos avançados, testes
    aufs, devicemapper, overlayLegados, removidos das versões atuais

    Descubra o seu com docker info (linhas Storage Driver e driver-type). Fonte: Docker docs · Select a storage driver e Docker Engine v29.

    Cada instrução, uma camada

    Container: camada de escrita efêmera · copy-on-write
    CMD ["nginx","-g","daemon off;"] só metadado, 0 B
    COPY index.html /usr/share/nginx/html/ camada
    RUN apk add --no-cache curl camada
    FROM nginx:stable-alpine camadas da base
    • RUN, COPY e ADD criam camadas. ENV, CMD, EXPOSE e LABEL só alteram a configuração.
    • As camadas são somente leitura e compartilhadas entre imagens e containers (union filesystem, overlay2).
    • Apagar um arquivo numa camada posterior não reduz a imagem: ele continua na camada anterior.

    Qual será a mais otimizada?

    Opção A: um único RUN encadeado
    Dockerfile
    FROM ubuntu:latest
    
    RUN apt-get install -y nginx \
        wget \
        curl \
        jq \
        && curl -o /usr/share/nginx/html/index.html \
           https://nginx.com/index.html
    
    ENTRYPOINT ["nginx", "-g", "daemon off;"]
    ↓ docker build
    Imagem · 2 camadas + 1 metadado
    ENTRYPOINT ["nginx", "-g", "daemon off;"]
    RUN apt-get install nginx wget curl jq && curl …
    FROM ubuntu:latest
    Opção B: um RUN por pacote
    Dockerfile
    FROM ubuntu:latest
    
    RUN apt-get install -y nginx
    RUN apt-get install -y wget
    RUN apt-get install -y curl
    RUN apt-get install -y jq
    RUN curl -o /usr/share/nginx/html/index.html \
           https://nginx.com/index.html
    
    ENTRYPOINT ["nginx", "-g", "daemon off;"]
    ↓ docker build
    Imagem · 6 camadas + 1 metadado
    ENTRYPOINT ["nginx", "-g", "daemon off;"]
    RUN curl -o …/index.html
    RUN apt-get install -y jq
    RUN apt-get install -y curl
    RUN apt-get install -y wget
    RUN apt-get install -y nginx
    FROM ubuntu:latest

    Menos camadas não é o único critério: o que mais pesa é o que fica dentro delas (cache do apt, arquivos temporários) e a ordem, que define o reaproveitamento do cache.

    Cache: do que muda menos para o que muda mais

    O build compara cada instrução com a do cache. Na primeira camada que mudar (ou cuja anterior já tenha mudado), o Docker para de reaproveitar e reconstrói dali para cima, na ordem do Dockerfile.

    Ruim: COPY . . antes de instalar

    Dockerfile
    FROM node:20-alpine
    COPY . .
    RUN npm install
    CMD ["node","server.js"]
    CMD
    RUN npm installreinstala tudo de novo
    COPY . .muda a cada commit
    FROM node:20-alpinecache

    Bom: manifesto antes do código

    Dockerfile
    FROM node:20-alpine
    COPY package.json package-lock.json ./
    RUN npm install
    COPY . .
    CMD ["node","server.js"]
    CMD
    COPY . .muda sempre, mas é rápida
    RUN npm installcache hit — pulado
    COPY package*.jsonsó muda com o manifesto
    FROM node:20-alpinecache

    Nos dois casos o COPY . . muda a cada commit — a diferença é o que está abaixo dele na pilha. No Bom, o npm install já rodou antes de copiar o código e continua em cache; no Ruim, ele é uma camada acima do código e é refeito toda vez.

    Prática
    Exercício 1.3

    Explorando a imagem: configuração, camadas e cache

    ~30 min

    Abrir a imagem criada no exercício 1.1 para ver na prática a anatomia OCI (tópico 1.4) e o efeito das camadas e do cache no tamanho e no tempo de build.

    Parte 1 · Anatomia da sua imagem

    1. O Cmd e o Entrypoint herdados da base:

      terminal
      docker image inspect "$IMAGE:1.0.0" --format 'Cmd={{json .Config.Cmd}} Entrypoint={{json .Config.Entrypoint}}'

      As labels OCI que você definiu no Dockerfile:

      terminal
      docker image inspect "$IMAGE:1.0.0" --format 'Labels={{json .Config.Labels}}'

      Quantas camadas a sua imagem tem:

      terminal
      docker image inspect "$IMAGE:1.0.0" --format '{{len .RootFS.Layers}} camadas'

      E quantas a imagem base já trazia:

      terminal
      docker image inspect nginx:stable-alpine --format '{{len .RootFS.Layers}} camadas na base'

      Por fim, o histórico completo de instruções:

      terminal
      docker history "$IMAGE:1.0.0"
    2. O documento que o registry devolve para a imagem base (index ou manifest list):

      terminal
      docker buildx imagetools inspect --raw nginx:stable-alpine | python3 -m json.tool | head -25
    3. Crie a pasta e entre nela:

      terminal
      mkdir -p ~/labs/oci && cd ~/labs/oci

      Exporte a sua imagem no formato OCI:

      terminal
      docker save "$IMAGE:1.0.0" -o site.tar

      Extraia o tar e liste o conteúdo:

      terminal
      tar -xf site.tar && ls

      Veja o oci-layout:

      terminal
      cat oci-layout; echo

      E o início do index.json:

      terminal
      python3 -m json.tool index.json | head -20

      Por fim, os blobs de conteúdo (camadas e config, endereçados por digest):

      terminal
      ls blobs/sha256 | head

    Parte 2 · Camadas e cache

    1. Veja qual backend de armazenamento o seu Docker usa:

      terminal
      docker info --format 'Storage Driver: {{.Driver}}'

      Com mais contexto (snapshotter, se for containerd image store):

      terminal
      docker info | grep -iA2 'storage driver'
    2. Crie a pasta e entre nela:

      terminal
      mkdir -p ~/labs/camadas && cd ~/labs/camadas

      Crie um Dockerfile que escreve e apaga um arquivo de 50 MB em instruções RUN separadas:

      terminal
      cat > Dockerfile.ruim <<'EOF'
      FROM alpine:3.22
      RUN dd if=/dev/zero of=/lixo bs=1M count=50
      RUN rm /lixo
      EOF

      Construa a imagem:

      terminal
      docker build -f Dockerfile.ruim -t camadas:ruim .

      Veja o tamanho de cada camada no histórico:

      terminal
      docker history camadas:ruim | head -4
    3. Agora o mesmo resultado, escrevendo e apagando o arquivo numa única instrução:

      terminal
      cat > Dockerfile.bom <<'EOF'
      FROM alpine:3.22
      RUN dd if=/dev/zero of=/lixo bs=1M count=50 && rm /lixo
      EOF

      Construa e compare o tamanho final das duas imagens:

      terminal
      docker build -f Dockerfile.bom -t camadas:bom .
      terminal
      docker image ls camadas
    4. Altere só o HTML do site:

      terminal
      cd ~/labs/site && sed -i 's/1.0.0/1.0.1/' index.html

      Reconstrua e observe quais etapas usam CACHED:

      terminal
      docker build -t "$IMAGE:1.0.1" .
    Resultado esperado
    • Sua imagem tem uma camada a mais que a base (a do COPY); CMD, LABEL e HEALTHCHECK aparecem no histórico com 0 B.
    • O mediaType da base é application/vnd.oci.image.index.v1+json; o docker save gera oci-layout, index.json e blobs/.
    • O docker info mostra overlayfs com driver-type: io.containerd.snapshotter.v1 (containerd image store) ou overlay2 (driver clássico).
    • camadas:ruim tem cerca de 60 MB; camadas:bom fica no tamanho do Alpine.
    • No último build, só o COPY em diante é refeito; o resto vem do cache.
    O que aprendemos com este exercício?
    • Cada instrução que altera arquivos vira uma camada; metadados como CMD e LABEL não ocupam espaço.
    • Apagar um arquivo numa camada posterior não diminui a imagem: ele continua na camada anterior.
    • A ordem das instruções define o aproveitamento do cache: o que muda pouco vem antes.
    • Uma imagem OCI em disco é oci-layout, index.json e blobs/.
    1.7

    Boas práticas e Hadolint

    Conceito

    Boas práticas de Dockerfile

    Base

    • Imagem mínima e oficial (slim, alpine, distroless)
    • Versão fixada, nunca latest; em produção, fixe o digest
    • Rebuild periódico para receber correções da base

    Build

    • Ordem pensando no cache
    • RUN encadeado e limpeza na mesma instrução (--no-cache, rm -rf /var/lib/apt/lists/*)
    • COPY em vez de ADD; .dockerignore
    • Multi-stage para separar build e runtime

    Execução

    • USER não-root
    • Forma exec em ENTRYPOINT/CMD para receber SIGTERM
    • Nenhum segredo em ENV, ARG ou camadas (use --secret)
    • LABELs OCI com origem e versão

    Hadolint: linter de Dockerfile

    Analisa o Dockerfile contra boas práticas (regras DLxxxx) e roda o ShellCheck nos comandos RUN (regras SCxxxx).

    O que valida

    • Sintaxe
    • Melhores práticas
    • Segurança
    • Consistência e manutenibilidade

    Exemplos de regras

    • DL3007 uso de latest
    • DL3008 pacotes sem versão
    • DL3020 ADD em vez de COPY
    • DL3025 CMD na forma shell

    Onde usar

    • Web: hadolint.github.io/hadolint
    • CLI ou container
    • Extensão do VS Code
    • CI: hadolint/hadolint-action
    Prática
    Exercício 1.4

    Validando Dockerfiles com Hadolint

    ~20 min

    Validar o Dockerfile do exercício 1.1, depois corrigir um Dockerfile cheio de más práticas até o Hadolint aprovar.

    1. Entre na pasta do Dockerfile criado no exercício 1.1:

      terminal
      cd ~/labs/site

      Valide-o com o Hadolint:

      terminal
      docker run --rm -i hadolint/hadolint < Dockerfile && echo "Dockerfile OK"
    2. Crie a pasta e entre nela:

      terminal
      mkdir -p ~/labs/lint && cd ~/labs/lint

      Crie uma aplicação mínima:

      terminal
      echo 'print("ola, containers")' > app.py

      Crie um Dockerfile cheio de más práticas:

      terminal
      cat > Dockerfile <<'EOF'
      FROM ubuntu:latest
      MAINTAINER aluno@example.com
      RUN apt-get update
      RUN apt-get install -y python3 curl
      ADD app.py /app/app.py
      WORKDIR /app
      CMD python3 app.py
      EOF

      Analise-o com o Hadolint:

      terminal
      docker run --rm -i hadolint/hadolint < Dockerfile
    3. Corrija até não restar aviso:

      terminal
      cat > Dockerfile <<'EOF'
      FROM python:3.13-alpine
      LABEL org.opencontainers.image.authors="aluno@example.com"
      WORKDIR /app
      COPY app.py .
      RUN adduser -D -u 10001 app
      USER app
      CMD ["python3", "app.py"]
      EOF

      Confira que o Hadolint aprova:

      terminal
      docker run --rm -i hadolint/hadolint < Dockerfile && echo "Dockerfile OK"

      Construa e rode:

      terminal
      docker build -t lint-ok . && docker run --rm lint-ok
    Resultado esperado
    • O Dockerfile do site passa (ou só gera avisos informativos).
    • O Dockerfile problemático aponta, entre outras, DL3007, DL4000, DL3008, DL3009, DL3015, DL3020, DL3025 e DL3059.
    • A versão corrigida imprime “Dockerfile OK” e o container mostra ola, containers.
    O que aprendemos com este exercício?
    • Um linter de Dockerfile pega más práticas antes do build: tag latest, pacotes sem versão, cache do apt esquecido.
    • Cada regra (DLxxxx) explica o problema e como corrigir.
    • O Hadolint pode virar uma etapa do pipeline: Dockerfile ruim nem chega a gerar imagem.
    1.8

    Tamanho, segurança e cadeia de suprimentos

    Conceito

    O tamanho da imagem importa muito

    App
    Dependências de build e de runtime
    Compiladores e SDK
    Cache do apt, arquivos temporários
    Ferramentas: curl, vim, shell…
    SO completo (ubuntu)
    ~2 GBimagem gorda
    App
    Só o runtime
    Base mínima
    ~300 MBimagem enxuta

    Mesma aplicação, 6,7× menos bytes.

    Numa rede de 1 Gbps (~125 MB/s), o pull em cada nó novo leva cerca de 16 s contra 2,4 s, antes mesmo de a aplicação começar a subir.

    Multiplique pelo número de nós, réplicas e deploys por dia.

    Escalabilidade

    Pull da imagem e cold start: cada novo nó ou pod precisa baixar a imagem.

    FinOps

    Transferência de dados e storage no registry e nos nós.

    Segurança

    Cada pacote a mais é uma CVE em potencial e um item de compliance.

    Pesquisa: as principais imagens base

    ImagemTamanhoPacotesCriticalHighMediumLowPullsMantenedor
    Ubuntu101 MB13100351B+Canonical
    Alpine8,51 MB2001221B+Alpine Linux
    Debian142 MB111011221B+Debian Project
    Fedora175 MB1530000100M+Red Hat
    AlmaLinux206 MB180004010M+AlmaLinux OS
    Arch Linux157 MB130013010M+Arch Linux Team
    Oracle Linux264 MB123000010M+Oracle
    Amazon Linux182 MB3012050M+AWS
    Wolfi14,3 MB1500001M+Chainguard

    Consulta realizada em maio de 2026, a partir do SBOM e do scan de cada imagem. Fonte: gersontpc/kubeautoday-talk (KubeAutoDay São Paulo, “Do Build ao Deploy: Containers Distroless Multi-Plataforma”).

    BusyBox: o canivete suíço de 2,5 MB

    Um único binário que implementa centenas de comandos Unix (sh, ls, wget, nslookup, vi, ps, tar…). É a base do Alpine e das imagens busybox, muito usada para troubleshooting e init containers.

    Útil para depurar, mas cada comando disponível na imagem de produção também está disponível para um atacante.

    Cadeia de suprimentos de software

    Tudo o que entra na sua imagem veio de algum lugar. A cadeia de suprimentos (software supply chain) é o caminho completo, do código até o container em produção, e cada elo pode introduzir uma falha ou um ataque.

    Códigoseu repositório
    →
    Dependênciaspip, npm, maven, go mod
    →
    Imagem baseFROM de terceiros
    →
    Build / CIpipeline, runners
    →
    Registrypush / pull
    →
    Deploycontainer rodando

    Casos reais

    • Log4Shell (2021): falha numa biblioteca Java presente em milhares de imagens
    • SolarWinds (2020): pipeline de build comprometido distribuiu código malicioso assinado
    • xz utils (2024): backdoor inserido numa dependência de sistema das distros Linux

    Como se proteger

    • Imagem base mínima, de fonte confiável e fixada por digest
    • Scan de vulnerabilidades no CI (Trivy)
    • SBOM de toda imagem publicada
    • Assinatura e proveniência (Sigstore/cosign, SLSA)

    SBOM: a lista de ingredientes da imagem

    Software Bill of Materials é o inventário de tudo o que compõe um artefato: pacotes do SO, bibliotecas da aplicação, versões, licenças e origem. Como o rótulo de um alimento, diz exatamente o que tem dentro.

    Para que serve

    • Saiu uma CVE nova: descobrir em minutos quais imagens usam a versão afetada
    • Auditoria e compliance de licenças
    • Exigido por regulações e contratos (governo dos EUA, setor financeiro)

    Formatos

    • SPDX (Linux Foundation, padrão ISO/IEC 5962)
    • CycloneDX (OWASP)
    • Ambos em JSON, anexáveis à imagem no registry como artefato OCI

    Como gerar

    • trivy image --format cyclonedx
    • syft <imagem>
    • docker buildx build --sbom=true (atestado no push)
    • docker scout sbom (substitui o antigo docker sbom)

    Imagens construídas com apko (como as do image-base e da pesquisa) geram SBOM automaticamente a cada build.

    A mesma ideia, numa caixa de cereal

    O rótulo de informação nutricional de uma embalagem já resolve, há décadas, o mesmo problema do SBOM: dizer exatamente o que tem dentro, para quem precisa decidir algo a partir disso.

    Rótulo da caixa de cereal
    Identificação
    Nome do produto, marca e lote
    Lista de itens
    Ingredientes, em ordem decrescente de quantidade
    Quantidade
    Gramas e % VD por porção
    Origem
    Fabricante, CNPJ, país de origem
    Alerta
    "Contém glúten", "contém leite"
    Validade
    Data de fabricação e validade
    Selo
    Registro ANVISA/Inmetro no rótulo
    SBOM da imagem
    Identificação
    Nome:tag da imagem e digest sha256
    Lista de itens
    Componentes: pacotes do SO e bibliotecas da aplicação
    Quantidade
    Versão exata de cada componente
    Origem
    Fornecedor de cada pacote (campo purl)
    Alerta
    CVEs conhecidas por componente
    Validade
    Data de geração do SBOM
    Selo
    Assinatura e proveniência (Sigstore/cosign, SLSA)

    A pergunta que o rótulo responde é sempre a mesma, só muda o produto: "isso é seguro para eu consumir?". Um recall de lote de alimento é o mesmo gesto que um alerta de CVE: olhar o rótulo/SBOM de tudo que está em uso e achar quem tem aquele item.

    Trivy: scan de vulnerabilidades

    O que encontra

    • CVEs em pacotes do SO e bibliotecas da aplicação
    • Misconfigurações (Dockerfile, Kubernetes, Terraform)
    • Segredos expostos e licenças

    Como usar no pipeline

    • --severity HIGH,CRITICAL
    • --exit-code 1 quebra o build
    • --ignore-unfixed foca no que tem correção
    • Gera SBOM (SPDX, CycloneDX)
    Prática
    Exercício 1.5

    Medindo o problema: reproduzindo a pesquisa com Trivy

    ~25 min

    Antes de otimizar, medir. Comparar tamanho, pacotes e vulnerabilidades de imagens base, avaliar a sua própria imagem, usar o Trivy como gate de pipeline e gerar o SBOM da imagem.

    1. Crie um alias para o Trivy (via container, com cache persistente):

      terminal
      alias trivy='docker run --rm -v /var/run/docker.sock:/var/run/docker.sock -v $HOME/.cache/trivy:/root/.cache aquasec/trivy:latest'

      Confira que funciona:

      terminal
      trivy --version
    2. Baixe algumas imagens base:

      terminal
      for i in ubuntu:24.04 debian:12 alpine:3.22 busybox:1.36 cgr.dev/chainguard/wolfi-base:latest; do docker pull -q $i; done

      Compare os tamanhos:

      terminal
      docker image ls --format 'table {{.Repository}}:{{.Tag}}\t{{.Size}}' | grep -E 'ubuntu|debian|alpine|busybox|wolfi'
    3. Conte pacotes e vulnerabilidades de cada uma:

      terminal
      for i in ubuntu:24.04 debian:12 alpine:3.22 cgr.dev/chainguard/wolfi-base:latest; do
        echo "== $i"
        trivy image --quiet --list-all-pkgs --format json $i | python3 -c "import json,sys; d=json.load(sys.stdin); print('pacotes:', sum(len(r.get('Packages',[])) for r in d.get('Results',[])))"
        trivy image --quiet --scanners vuln $i | grep -E '^Total'
      done
    4. Avalie a sua imagem:

      terminal
      trivy image --severity HIGH,CRITICAL "$IMAGE:1.0.1"

      Baixe uma imagem antiga e cheia de CVEs conhecidas:

      terminal
      docker pull -q python:3.4-alpine

      Use o exit code do Trivy como gate de pipeline:

      terminal
      trivy image --exit-code 1 --severity HIGH,CRITICAL --quiet python:3.4-alpine > /dev/null; echo "python:3.4-alpine → exit $?"
    5. Gere o SBOM da sua imagem:

      terminal
      trivy image --quiet --format cyclonedx "$IMAGE:1.0.1" > ~/labs/sbom-site.json

      Consulte o inventário de componentes:

      terminal
      python3 -c "import json; d=json.load(open('$HOME/labs/sbom-site.json')); c=d['components']; print(len(c),'componentes'); [print(x['name'],x.get('version','')) for x in c if x['name'] in ('openssl','libssl3','musl','busybox','nginx','zlib')]"
    Resultado esperado
    • A ordem de tamanho segue a pesquisa: Ubuntu e Debian na casa das dezenas de MB, Alpine e Wolfi poucos MB, BusyBox o menor.
    • Wolfi e Alpine têm poucas dezenas de pacotes; Ubuntu e Debian, mais de cem.
    • python:3.4-alpine retorna exit 1: num pipeline, o build seria barrado.
    • O SBOM lista dezenas de componentes da sua imagem com versão; é com ele que se responde “estamos afetados?” quando sai uma CVE nova.
    O que aprendemos com este exercício?
    • A imagem base define boa parte do tamanho, dos pacotes e das vulnerabilidades da sua imagem.
    • Medir antes de otimizar: tamanho, número de pacotes e CVEs por severidade.
    • O --exit-code transforma o Trivy num gate de pipeline.
    • O SBOM é o inventário da imagem: responde rápido “estamos afetados?” quando sai uma CVE nova.
    1.9

    Multi-stage e distroless

    Conceito

    Separar o que constrói do que executa

    O multi-stage divide a construção em etapas independentes no mesmo Dockerfile. No final, só os artefatos necessários são copiados para a imagem de produção.

    Menor

    Compiladores, SDKs e bibliotecas de desenvolvimento ficam fora da imagem final.

    Mais segura

    Sem ferramentas de build nem arquivos temporários ou sensíveis na imagem publicada.

    Build mais rápido

    Etapas independentes rodam em paralelo e aproveitam cache.

    Como funciona

    Etapa 1: buildFROM golang:1.25-alpine AS build · compila o binário
    →
    COPY --from=buildcopia só o artefato
    →
    Etapa 2: produçãoFROM distroless/static · binário + usuário nonroot
    • Cada FROM inicia uma etapa; AS nome permite referenciá-la.
    • docker build --target build para parar numa etapa (útil para testes).
    • Vale para qualquer linguagem: pip install --user numa etapa e cópia de /root/.local na outra, npm ci + cópia do dist/, mvn package + cópia do .jar.

    Distroless

    Imagens que contêm apenas a aplicação e suas dependências de runtime: sem shell, sem gerenciador de pacotes, sem utilitários como curl, wget ou netcat. Se um componente não está lá, ele não pode ser explorado.

    Opções

    • Google distroless (gcr.io/distroless/*)
    • Chainguard / Wolfi
    • Construção própria com apko + melange, sem Dockerfile

    gersontpc/image-base

    • Java, Python, Go, Node.js e .NET, amd64 e arm64
    • Camada única, non-root, SBOM a cada build
    • Trivy barra CVEs HIGH/CRITICAL antes do push

    Benefícios observados na pesquisa: menos CVEs, pull mais rápido e redução de custo de 20% a 30%. Sem shell, a depuração usa ephemeral containers (Aula 05).

    Prática
    Exercício 1.6

    Resolvendo: da imagem grande à distroless

    ~35 min

    Construir a mesma aplicação Go em estágio único e em multi-stage com distroless, e provar a diferença com os números e com o Trivy. Guarde esta pasta: a aplicação volta na Aula 05.

    1. Crie a pasta e entre nela:

      terminal
      mkdir -p ~/labs/app-go && cd ~/labs/app-go

      Crie o go.mod:

      terminal
      printf 'module example.com/app-go\n\ngo 1.25\n' > go.mod

      Crie a aplicação (responde em / e /healthz e trata SIGTERM):

      terminal
      cat > main.go <<'EOF'
      package main
      
      import (
          "context"
          "fmt"
          "log"
          "net/http"
          "os"
          "os/signal"
          "syscall"
          "time"
      )
      
      func main() {
          host, _ := os.Hostname()
          version := os.Getenv("APP_VERSION")
          if version == "" {
              version = "v1"
          }
          mux := http.NewServeMux()
          mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
              fmt.Fprintf(w, "Container Technologies | versao=%s | host=%s\n", version, host)
          })
          mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
              w.Write([]byte("ok\n"))
          })
          srv := &http.Server{Addr: ":8080", Handler: mux}
          go func() {
              log.Printf("ouvindo em :8080 (versao %s)", version)
              if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
                  log.Fatal(err)
              }
          }()
          stop := make(chan os.Signal, 1)
          signal.Notify(stop, syscall.SIGTERM, os.Interrupt)
          <-stop
          log.Println("SIGTERM recebido, encerrando conexoes...")
          ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
          defer cancel()
          srv.Shutdown(ctx)
      }
      EOF
    2. Crie a versão em estágio único:

      terminal
      cat > Dockerfile.single <<'EOF'
      FROM golang:1.25
      WORKDIR /src
      COPY . .
      RUN go build -o /usr/local/bin/app .
      EXPOSE 8080
      CMD ["app"]
      EOF

      Construa-a:

      terminal
      docker build -f Dockerfile.single -t app-go:single .
    3. Crie a versão multi-stage com distroless:

      terminal
      cat > Dockerfile <<'EOF'
      FROM golang:1.25-alpine AS build
      WORKDIR /src
      COPY go.mod ./
      RUN go mod download
      COPY . .
      RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /out/app .
      
      FROM gcr.io/distroless/static-debian12:nonroot
      COPY --from=build /out/app /app
      USER nonroot:nonroot
      EXPOSE 8080
      ENTRYPOINT ["/app"]
      EOF

      Construa-a:

      terminal
      docker build -t app-go:v1 .

      Compare o tamanho das duas:

      terminal
      docker image ls app-go
    4. Rode a imagem final:

      terminal
      docker run -d --name go1 -p 8081:8080 app-go:v1

      Teste os dois endpoints:

      terminal
      curl -s localhost:8081; curl -s localhost:8081/healthz

      Tente abrir um shell (deve falhar: não há shell na imagem):

      terminal
      docker exec -it go1 sh

      Confira que roda como usuário não-root:

      terminal
      docker image inspect app-go:v1 -f 'User={{.Config.User}}'

      Pare, veja o log de encerramento e remova:

      terminal
      docker stop go1 && docker logs go1 | tail -2 && docker rm go1
    5. Prove com o Trivy (alias do exercício 1.5) a diferença de vulnerabilidades entre as duas imagens:

      terminal
      for i in app-go:single app-go:v1; do echo "== $i"; trivy image --quiet --scanners vuln $i | grep -E '^Total' | head -3; done

      Use a distroless como gate de pipeline:

      terminal
      trivy image --exit-code 1 --severity HIGH,CRITICAL --quiet app-go:v1 > /dev/null; echo "app-go:v1 → exit $?"
    Resultado esperado
    • app-go:single passa de 800 MB; app-go:v1 fica abaixo de 10 MB.
    • docker exec … sh retorna executable file not found in $PATH, e a imagem roda como nonroot:nonroot.
    • O Trivy encontra bem menos vulnerabilidades na distroless, e o gate passa com exit 0.
    O que aprendemos com este exercício?
    • O multi-stage separa build e execução: o compilador fica no primeiro estágio e só o binário vai para a imagem final.
    • A distroless não tem shell nem gerenciador de pacotes e roda como nonroot: menos superfície de ataque.
    • O ganho é mensurável: de centenas de MB para poucos MB, e bem menos vulnerabilidades.
    • A contrapartida é a depuração: sem shell, ela exige outras técnicas (ephemeral container, na Aula 05).
    Desafio

    Reproduza o lab de apko + melange do repositório kubeautoday-talk: uma imagem Python distroless multi-arquitetura, sem Dockerfile, com 1 camada. Ou troque a base final por scratch e descubra o que se perde.

    1.10

    Tags, versionamento e registry

    Conceito

    Anatomia de uma referência

    docker.io/usuario/container-technologies:1.4.2

    Registry

    Docker Hub (padrão), Amazon ECR, GHCR, Harbor, Artifact Registry, ACR.

    Repositório

    Usuário ou organização + nome da aplicação.

    Tag × digest

    Tag é legível e mutável. Digest (@sha256:…) é imutável.

    Boas práticas de tag

    TagUso
    1.4.2Versão exata (SemVer). É o que vai para produção
    1.4 / 1Ponteiros para a última correção da série, úteis para consumidores
    v1.4.2-3f9c2abRecomendado: versão + SHA curto do commit. Legível, rastreável até o autor e nunca reaproveitada
    sha-3f9c2abCommit que gerou a imagem: rastreabilidade do código ao container
    stable + 010726-0152Canal móvel + referência imutável por data (padrão do image-base)
    latestEvite em produção: não diz qual versão está rodando nem permite rollback confiável
    • Build once, deploy many: a mesma imagem (mesmo digest) é promovida de dev para produção.
    • Ative tag immutability no registry (o ECR suporta) para impedir sobrescrita.
    • Autentique com token de acesso, nunca com a senha da conta.

    SemVer + Conventional Commits

    Semantic Versioning

    MAJOR.MINOR.PATCH

    • MAJOR: mudança incompatível
    • MINOR: funcionalidade compatível
    • PATCH: correção compatível
    • Pré-release: 2.0.0-rc.1

    Conventional Commits

    • fix: corrige timeout → PATCH
    • feat: adiciona /healthz → MINOR
    • feat!: remove API v1 ou BREAKING CHANGE: → MAJOR
    • chore:, docs:, ci: não geram versão

    Com commits semânticos, ferramentas como semantic-release e release-please calculam a próxima versão e a tag da imagem automaticamente no pipeline.

    Prática
    Exercício 1.7

    Publicando no Docker Hub, inclusive multi-plataforma

    ~35 min

    Fechar o ciclo da imagem: publicar com uma estratégia de tags, conferir os digests, executar a partir do registry e gerar uma versão para amd64 e arm64 (o índice OCI do tópico 1.2).

    Parte 1 · Tags e push

    1. Entre na pasta da imagem:

      terminal
      cd ~/labs/site

      No Docker Hub, crie um Personal access token (Account settings → Personal access tokens) e o repositório container-technologies. Depois faça login:

      terminal
      docker login -u "$DOCKERHUB_USER"      # cole o token quando pedir a senha
    2. Defina a versão e o SHA curto do commit:

      terminal
      VERSION=1.0.1
      SHA=$(git rev-parse --short HEAD 2>/dev/null || echo local)

      Aplique as tags de série, commit e a tag recomendada (versão + SHA) à mesma imagem:

      terminal
      docker tag "$IMAGE:$VERSION" "$IMAGE:1.0"
      docker tag "$IMAGE:$VERSION" "$IMAGE:sha-$SHA"
      docker tag "$IMAGE:$VERSION" "$IMAGE:v$VERSION-$SHA"      # recomendado: vMAJOR.MINOR.PATCH-sha

      Publique todas as tags:

      terminal
      docker push --all-tags "$IMAGE"
    3. Confirme que as tags apontam para o mesmo digest:

      terminal
      for t in 1.0.1 1.0 sha-$SHA v1.0.1-$SHA; do echo -n "$t → "; docker buildx imagetools inspect "$IMAGE:$t" --format '{{json .Manifest.Digest}}'; done

      Remova as imagens locais para forçar o pull do registry:

      terminal
      docker image rm $(docker image ls "$IMAGE" -q) --force

      Rode a partir do registry:

      terminal
      docker run -d --name site -p 8080:80 "$IMAGE:1.0.1" && sleep 2

      Confira a resposta e limpe:

      terminal
      curl -s localhost:8080 | grep Versão; docker rm -f site

    Parte 2 · Uma imagem, várias plataformas

    1. Confira a arquitetura do host:

      terminal
      uname -m

      Habilite a emulação com QEMU:

      terminal
      docker run --privileged --rm tonistiigi/binfmt --install arm64

      Rode uma imagem arm64 no mesmo host amd64:

      terminal
      docker run --rm --platform linux/arm64 alpine:3.22 uname -m
    2. Altere a versão exibida pela página:

      terminal
      sed -i 's/1.0.1/1.1.0/' index.html

      Crie um builder multi-plataforma:

      terminal
      docker buildx create --name multi --use

      Construa e publique a versão 1.1.0 para as duas arquiteturas:

      terminal
      docker buildx build --platform linux/amd64,linux/arm64 -t "$IMAGE:1.1.0" --push .

      Confira o índice gerado:

      terminal
      docker buildx imagetools inspect "$IMAGE:1.1.0"
    Resultado esperado
    • O Docker Hub mostra as tags 1.0.0, 1.0.1, 1.0, sha-… e 1.1.0; as três do passo 3 têm o mesmo digest.
    • O container baixado do registry responde “Versão da aplicação: 1.0.1”.
    • Com --platform linux/arm64 a saída é aarch64, e a 1.1.0 é um índice com manifests linux/amd64 e linux/arm64.
    O que aprendemos com este exercício?
    • A tag é um ponteiro que pode mudar; o digest identifica o conteúdo de forma imutável.
    • Uma boa estratégia de tags combina a versão completa, a versão parcial e o SHA do commit, para rastrear a origem.
    • O buildx gera um índice multi-plataforma (amd64 e arm64) com um só nome.
    • Rodar a imagem direto do registry prova que o que foi publicado funciona.
    Dica

    Para não digitar o token, crie os secrets DOCKERHUB_USER e DOCKERHUB_TOKEN em Settings → Codespaces e use echo "$DOCKERHUB_TOKEN" | docker login -u "$DOCKERHUB_USER" --password-stdin. Entregue o link do repositório no portal.

    1.11

    Documentação de referência

    Leituras

    Para aprofundar

    Padrões

    • Semantic Versioning (SemVer)
    • Conventional Commits (semantic commit)
    • OCI image-spec
    • OCI runtime-spec
    • OCI distribution-spec
    • SPDX e CycloneDX (SBOM)

    Docker

    • Referência do Dockerfile
    • Boas práticas de build
    • Multi-stage builds
    • Cache de build
    • Imagens multi-plataforma

    Ferramentas

    • Hadolint
    • Trivy
    • SLSA e Sigstore (cadeia de suprimentos)
    • Google distroless
    • apko e melange
    • runc

    Repositórios da disciplina

    • gersontpc/image-base
    • gersontpc/kubeautoday-talk