01 - Docker (Imagens, Contêineres, Dockerfile)
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 512mfunciona, 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
latestcomo 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
COPYeARGdeixam rastro na camada; use BuildKit secrets comRUN --mount=type=secret. - Rodar como root, ampliando o impacto de uma exploração; defina
USERe, 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.