Home Comunidade Eventos YouTube Networks Desafios // produtos Academy News CTI | CVE Contato

01 - Docker (Imagens, Contêineres, Dockerfile)

#containers#docker#dockerfile#imagens#registry
video explicativo
Video explicativoEspaco reservado para o video desta anotação — adicione video: https://youtu.be/... no frontmatter da nota.

Docker (Imagens, Contêineres, Dockerfile)

Docker é a plataforma que padronizou a construção, distribuição e execução de contêineres em sistemas Linux. Um contêiner empacota a aplicação com suas dependências em um processo isolado do sistema operacional, compartilhando o kernel do host. A imagem é o artefato imutável que descreve esse pacote; o contêiner é a instância em execução; e o Dockerfile é a receita que gera a imagem de forma reproduzível. Em produção, essa tríade resolve o clássico problema de "funciona na minha máquina", pois o mesmo artefato testado em desenvolvimento chega idêntico ao ambiente de staging e de produção. A relevância prática é direta: quase todo pipeline de CI/CD moderno termina em uma imagem empurrada para um registry e executada por um orquestrador como Kubernetes ou ECS. Sem domínio de Docker, o time perde controle sobre tempo de build, tamanho de imagem, consumo de memória e superfície de ataque.

Conceito e problema que resolve

Antes dos contêineres, o empacotamento de software dependia de ambientes virtuais completos ou de scripts de provisionamento frágeis. A máquina virtual abstrai hardware e carrega um sistema operacional inteiro, com overhead de gigabytes e minutos de inicialização. O contêiner abstrai o sistema operacional: usa namespaces para isolar processos, rede e sistema de arquivos, e cgroups para limitar CPU e memória. O resultado é isolamento com custo de processo simples, iniciando em milissegundos e ocupando dezenas de megabytes.

O problema central resolvido é a reprodutibilidade. Uma imagem Docker contém o runtime da linguagem, bibliotecas nativas, variáveis de ambiente e o comando de entrada, tudo versionado por digest. Dois ambientes que executam a mesma imagem, com a mesma configuração externa, comportam-se de forma equivalente. Isso reduz o atrito entre times de desenvolvimento, QA e operações, e viabiliza o princípio de imutabilidade: contêineres em produção não são alterados, são substituídos.

Imagens em camadas e o cache de build

Uma imagem Docker é composta de camadas somente leitura empilhadas. Cada instrução do Dockerfile gera uma camada, e o builder reutiliza camadas cuja entrada não mudou. Esse mecanismo de cache é a principal alavanca de velocidade de build: instruções que mudam pouco devem ficar no topo do arquivo, e instruções que mudam sempre, como cópia do código-fonte, devem ficar no final. A ordem errada das instruções invalida o cache inteiro e transforma um build de segundos em minutos.

Camadas têm consequências de tamanho. Comandos como apt-get install seguido de remoção de caches em instruções separadas criam duas camadas, e a primeira ainda contém os dados removidos. A prática correta é encadear comandos na mesma instrução com &&, ou usar RUN --mount=type=cache com BuildKit, que mantém o cache fora das camadas da imagem final. Arquivos .dockerignore evitam copiar dependências locais, segredos e o diretório .git para o contexto de build.

Multi-stage builds

O multi-stage build permite usar uma imagem rica para compilar e uma imagem mínima para executar. O estágio de build traz compiladores, headers e ferramentas; o estágio final copia apenas o artefato produzido com COPY --from. Uma aplicação Go compilada estaticamente pode rodar em uma imagem scratch de poucos megabytes; uma aplicação Python pode rodar em python:3.12-slim sem pip, gcc ou código-fonte desnecessário. O ganho é triplo: imagens menores reduzem pull em escala, diminuem superfície de ataque e aceleram cold start em orquestradores.

# Estágio 1: build da aplicação Java com Maven e JDK completos
FROM maven:3.9-eclipse-temurin-21 AS build
WORKDIR /app
# Copia primeiro o pom.xml para preservar o cache de dependências
COPY pom.xml .
RUN mvn dependency:go-offline -q
# Copia o código-fonte por último, pois é o que muda com mais frequência
COPY src ./src
RUN mvn package -DskipTests -q

# Estágio 2: runtime mínimo, apenas JRE, sem Maven nem código-fonte
FROM eclipse-temurin:21-jre-alpine
# Usuário sem privilégios reduz o impacto de um eventual escape do contêiner
RUN addgroup -S app && adduser -S app -G app
USER app
WORKDIR /app
# Copia somente o jar do estágio de build para a imagem final
COPY --from=build /app/target/app.jar ./app.jar
EXPOSE 8080
# exec form garante que o processo receba sinais do sistema (SIGTERM)
ENTRYPOINT ["java", "-jar", "app.jar"]

Funcionamento interno do runtime

Compreender o que acontece ao executar docker run ajuda a diagnosticar problemas reais. O daemon orquestra três primitivas do kernel Linux: namespaces, cgroups e sistemas de arquivos empilhados.

  • Namespaces de PID, rede, montagem, UTS, usuário e IPC isolam a visão que o processo tem do sistema; um contêiner vê o PID 1 como seu processo principal e sua própria pilha de rede.
  • Cgroups limitam e contabilizam CPU, memória, I/O e pids; é por isso que --memory 512m funciona, e estourar esse limite dispara OOM kill.
  • OverlayFS empilha camadas: as camadas da imagem são somente leitura e uma camada de escrita do contêiner recebe todas as alterações em copy-on-write.
  • O driver de storage e o runtime de baixo nível, hoje containerd com runc, executam a criação do ambiente e o spawn do processo.

Essa anatomia explica comportamentos que confundem times em produção. Um contêiner que grava muito na camada de escrita sofre degradação de I/O; um que não define limites compete por memória com o host; e um processo que ignora SIGTERM é morto com SIGKILL após o grace period, perdendo requisições em voo.

Aplicação prática no dia a dia

A linha de comando cobre o ciclo completo: construir, inspecionar, executar e depurar. Os comandos abaixo representam o repertório mínimo de quem opera contêineres em produção.

# Constrói a imagem com tag versionada e imprime o tamanho por camada
docker build -t acme/pedidos:1.4.2 . && docker image ls acme/pedidos

# Inspeciona o histórico de camadas para entender o que inflou a imagem
docker history acme/pedidos:1.4.2 --no-trunc

# Executa o contêiner com limites explícitos, rede própria e health check
docker run -d --name pedidos \
  --memory 512m --cpus 0.5 \
  --read-only --tmpfs /tmp \
  -p 8080:8080 \
  acme/pedidos:1.4.2

# Acompanha logs em tempo real e variáveis efetivas do contêiner
docker logs -f pedidos && docker inspect pedidos --format '{{.Config.Env}}'

# Depuração interativa em um contêiner que já está em execução
docker exec -it pedidos sh

# Remove imagens e camadas órfãs que acumulam no host
docker image prune -f && docker builder prune -f

Na prática, o fluxo de produção não usa docker run diretamente; ele usa compose localmente e um orquestrador em produção. Ainda assim, dominar esses comandos é o que permite debugar uma imagem antes do push, verificar se o usuário está correto, se o processo escuta na porta certa e se os limites estão coerentes com o perfil de carga.

Dockerfile instrução a instrução

As instruções essenciais formam um vocabulário pequeno, mas cada uma tem pegadinhas em produção. A tabela resume as principais.

Instrução Função Cuidado em produção
FROM Define a imagem base e inicia um estágio Prefira tags específicas e digest; evite latest
RUN Executa comandos no build e cria camada Encadeie com && e limpe caches na mesma camada
COPY Copia arquivos do contexto para a imagem Prefira a ADD; evite copiar segredos
ADD Copia com extração de tar e suporte a URL Use só quando precisar descompactar
WORKDIR Define diretório de trabalho das etapas seguintes Sempre caminho absoluto
ENV Variável de ambiente persistente na imagem Não coloque segredos; use runtime
ARG Variável somente durante o build Diferença de ENV confunde iniciantes
EXPOSE Documenta a porta de escuta Não publica porta; é documentação
ENTRYPOINT Processo principal do contêiner Use exec form para receber sinais
CMD Argumentos padrão do ENTRYPOINT Sobrescrito facilmente; comporte-se bem
USER Define usuário de execução Nunca rode como root por padrão
HEALTHCHECK Comando de verificação de saúde Opcional; orquestradores têm seus próprios probes

Registry, tags e versionamento

O registry é o repositório de imagens; Docker Hub, Amazon ECR, Harbor e GitLab Container Registry são exemplos comuns. Uma referência de imagem completa tem a forma registro/nome:tag, e cada push gera um digest imutável do manifesto. Tags como 1.4.2, 1.4 e latest podem apontar para conteúdos diferentes ao longo do tempo; o digest é a única referência realmente imutável. Em produção, pipelines devem promover imagens por digest ou por tags semânticas geradas pelo CI, nunca reconstruir a "mesma" tag com conteúdo novo.

A estratégia de tags deve cobrir três necessidades: rastreabilidade até o commit, capacidade de reverter e clareza do ambiente. Uma tag como 1.4.2 ou sha-a1b2c3 atende rastreabilidade e reversão; latest serve apenas a ambientes locais. Políticas de retenção no registry, como lifecycle policies no ECR, controlam o custo do acúmulo de camadas antigas.

Volumes, redes e estado

Contêineres são efêmeros por design: dados escritos dentro do contêiner se perdem na substituição. Volumes resolvem o problema de estado persistente, bind mounts resolvem o desenvolvimento com hot reload, e tmpfs cobre segredos temporários em memória. Em orquestração, os volumes se tornam objetos gerenciados, com provisionamento via StorageClass. A regra de produção é tratar o contêiner como stateless sempre que possível, empurrando o estado para bancos gerenciados ou armazenamento de objetos.

Redes de contêiner fornecem isolamento e descoberta por nome. No modo bridge padrão, contêineres na mesma rede se alcançam pelo nome de serviço; o modo host dispensa NAT ao custo de isolamento. Em Kubernetes e ECS, o modelo de rede é gerenciado pela plataforma, com Service, Target Group e network policy cuidando da descoberta e do controle de tráfego. Evitar publicar portas desnecessárias e restringir tráfego entre namespaces é parte da higiene de rede em produção.

Contêiner versus máquina virtual

Critério Contêiner Máquina virtual
Camada de abstração Sistema operacional Hardware
Kernel Compartilhado com o host Próprio de cada VM
Tamanho típico Dezenas de MB Vários GB
Tempo de inicialização Milissegundos a segundos Minutos
Densidade por host Dezenas a centenas Poucas a dezenas
Isolamento Namespaces e cgroups Hipervisor, mais forte
Portabilidade Imagem executada em qualquer host compatível Imagem ligada ao hipervisor

A comparação não é adversarial: em nuvem, as VMs são o substrato sobre o qual os contêineres rodam. Fargate e ECS Managed Instances ilustram a convergência, entregando contêineres sobre um pool de infraestrutura gerenciado, com casos que pedem GPU, containers privileged ou memória acima do teto do Fargate resolvidos com instâncias dedicadas.

Erros comuns e boas práticas

Os erros mais frequentes aparecem no Dockerfile e na operação diária. Reconhecê-los evita incidentes recorrentes.

  • Usar latest como tag de deploy, o que torna o rollback imprevisível e a auditoria impossível; prefira tags imutáveis geradas pelo pipeline.
  • Copiar segredos no build, pois COPY e ARG deixam rastro na camada; use BuildKit secrets com RUN --mount=type=secret.
  • Rodar como root, ampliando o impacto de uma exploração; defina USER e, no Kubernetes, runAsNonRoot.
  • Despejar logs em arquivos internos; contêineres devem logar em stdout e stderr para coleta pelo runtime.
  • Imagens gigantes com toolchain de build incluída; multi-stage e imagens distroless cortam o tamanho e o ataque.
  • Ignorar sinais do sistema com shell form no ENTRYPOINT, gerando encerramento abrupto no scale down; use exec form.
  • Não definir limites de memória e CPU, permitindo que um contêiner degrade o host; cgroups e limites do orquestrador existem para isso.

Boas práticas consolidadas reduzem custo e risco. Mantenha o Dockerfile mínimo, documentado e estável; cada instrução deve ter propósito claro. Fixe versões de base e atualize com cadência para incorporar patches de CVE. Meça tamanho e tempo de build como métricas de qualidade, e trate regressões como defeito. Por fim, teste a imagem como artefato, executando os mesmos probes de saúde que a produção exigirá, e não apenas os testes unitários do código.