CURSOS / TECNOLOGIA← Voltar aos cursos
Contêineres e Operação · Curso prático

Docker em
produção com critério.

Aprenda a construir imagens enxutas, isolar memória e CPU no kernel Linux, diagnosticar falhas sem adivinhação e promover o mesmo artefato imutável do seu notebook até o cluster.

Estúdio arquitetônico com racks de servidores modernos e módulo de contêiner em verde-petróleo
8 oficinascom comandos reais e casos de produção
1 projetopara empacotar e validar em uma semana
Seu cadernocom diagnósticos, limites e decisões

Percurso de aprendizagem

Comece com um serviço que você já conhece — uma API web, uma fila de processamento ou um script agendado. Você pode testar os comandos em uma estação Linux ou no Docker Desktop. Os estudos de caso, os cálculos de capacidade e os diagnósticos comentados podem ser praticados diretamente aqui.

PODCAST DO CURSO · ÁUDIO NA PÁGINA
Ilustração editorial de gato frajolinha com fones sentado sobre um contêiner

Podcast — Como o Docker funciona por dentro

Episódio complementar sobre kernel Linux, namespaces, cgroups e camadas de imagens para ouvir direto aqui, sem sair da página.

Estação de trabalho de engenharia com terminal aberto, caderno quadriculado de arquitetura e iluminação natural

“Empacotar uma aplicação em um contêiner é trocar servidores artesanais por artefatos verificáveis.”

Inspirado na abordagem prática de Sean P. Kane e Karl Matthias em Docker: Up & Running (3ª edição): cada oficina combina o funcionamento real do kernel Linux com uma decisão operacional que você pode testar hoje.
Oficina 01 / 08

Descubra o que um contêiner isola e onde o estado mora

Imagine que uma equipe empacotou um sistema financeiro em um contêiner, comemorou o deploy em três segundos e, na primeira atualização da imagem, perdeu todos os comprovantes enviados pelos clientes naquela manhã. O que ficou dentro do contêiner que jamais deveria morar lá?

Processo isolado no kernel, não uma miniatura de servidor

Kane e Matthias abrem o livro com a analogia do contêiner marítimo padronizado na década de 1950: antes dele, cada carga exigia estiva manual no porto; depois dele, guindastes, navios e caminhões passaram a manipular a mesma caixa selada sem importar se lá dentro viajam grãos ou eletrônicos. No software, uma imagem Docker empacota o binário da aplicação e todas as bibliotecas do sistema de arquivos em um artefato único que roda da mesma forma no notebook do desenvolvedor, no servidor de testes e em produção.

Para operar em produção sem surpresas, porém, é preciso abandonar a ideia de que um contêiner funciona como uma máquina virtual leve. Uma máquina virtual exige um hipervisor e carrega um sistema operacional convidado completo, com kernel próprio, consumindo gigabytes de disco e centenas de megabytes de RAM antes mesmo de abrir sua aplicação. Já um contêiner Linux é um processo comum do próprio kernel do host, isolado por namespaces (que restringem o que ele enxerga) e limitado por cgroups (que controlam quanto hardware ele pode consumir). Criar um novo contêiner a partir de uma imagem já baixada adiciona uma camada gravável de cerca de 12 KB no disco e inicia na velocidade de um comando nativo.

Essa arquitetura também explica a cadeia de ferramentas nos bastidores: quando você digita um comando no cliente (docker), ele conversa via API REST sobre socket Unix com o daemon (dockerd), que delega o gerenciamento do ciclo de vida ao containerd e aciona o runtime compatível com a especificação OCI (como o runc) para criar o contêiner no kernel e sair do caminho. O efeito prático mais importante para quem projeta sistemas é a efemeridade: contêineres devem ser descartáveis. Qualquer estado durável precisa viver fora da camada gravável do contêiner.

Oficina de arquitetura: o que vai na imagem e o que fica fora

Analise estes três componentes de um sistema de emissão de boletos de uma distribuidora. Antes de olhar o comando abaixo, decida onde cada elemento deve residir para que qualquer contêiner possa ser destruído e recriado em segundos sem perda de dados.

Decisões de fronteira ao empacotar um serviço
Componente do sistemaO que acontece se ficar dentro do contêinerDecisão de produção
Binário da API e bibliotecas Python fixadasPermanece idêntico em todas as réplicas e versõesCongelar dentro da imagem (camadas imutáveis somente leitura).
PDFs de comprovantes enviados pelos clientesDesaparecem quando o contêiner é substituído por uma nova versãoGravar em armazenamento externo (S3/Object Storage) ou em docker volume dedicado.
Credenciais do banco de dados de produçãoVazam para quem tiver permissão de docker pull no registro de imagensInjetar apenas em tempo de execução via variáveis de ambiente ou gerenciador de segredos.
Comando para experimentar
# 1. Verifique a comunicação entre o cliente (CLI) e o servidor (daemon)
docker version
docker context ls

# 2. Inicie um contêiner interativo descartável (--rm) com Ubuntu 22.04
docker container run --rm -ti --name lab-efemero ubuntu:22.04 /bin/bash

# 3. Dentro do contêiner, observe que o bash é o PID 1 isolado e crie um arquivo de teste:
ps aux
echo "comprovante-teste" > /tmp/recibo.txt
exit

# 4. Tente encontrar o contêiner ou o arquivo após o exit:
docker container ls -a

Repita o teste sem a flag --rm e depois crie dois contêineres a partir da mesma imagem ubuntu:22.04. Você notará que o arquivo criado em um não aparece no outro e que as camadas originais da imagem são compartilhadas no disco sem duplicação.

Laboratório avançado · Por que contêineres que guardam estado local impedem atualizações seguras?

Quando uma equipe trata um contêiner como uma máquina virtual e aplica correções manualmente dentro dele (usando apt-get upgrade com o contêiner rodando ou salvando sessões de usuários em /var/lib local), surgem dois problemas imediatos. Primeiro, na próxima vez que o orquestrador reiniciar o contêiner em outro servidor, todas as alterações feitas na camada gravável somem. Segundo, se você tentar salvar esse contêiner alterado com docker container commit, perderá a receita reproduzível de como ele foi construído.

A regra operacional do livro é direta: atualizações nunca são feitas dentro de um contêiner em execução. Você altera o código ou o Dockerfile, constrói uma nova imagem verificável, sobe o novo contêiner conectado aos mesmos volumes ou bancos externos e destrói o antigo.

Entrega da etapa

Escolha um serviço da sua equipe ou um projeto pessoal. Liste três coisas que são imutáveis (código, dependências, ativos estáticos), duas que representam estado persistente (banco, uploads, fila) e duas configurações que mudam entre desenvolvimento e produção. Escreva o comando docker container run inicial para subir apenas a parte efêmera.

Conexão com o livro · Capítulos 1–3 · surgimento dos contêineres, arquitetura do Docker, diferenças entre VMs e processos isolados no Linux.

Oficina 02 / 08

Construa imagens enxutas com cache inteligente e múltiplos estágios

Uma imagem de 850 MB com compilador C, gerenciador de pacotes e arquivos temporários demora para baixar no cluster e oferece dezenas de ferramentas prontas caso um invasor explore uma falha na aplicação. Como entregar apenas o binário estático em 4 MB?

Camadas aditivas, ordem de cache e builds multiestágio

Cada instrução de um Dockerfile (como FROM, COPY e RUN) gera uma nova camada somente leitura empilhada sobre a anterior por meio de um sistema de arquivos de união (como o overlay2). O detalhe que mais surpreende equipes em produção é que camadas são puramente aditivas: se uma linha RUN baixa um arquivo de 200 MB e a linha RUN seguinte executa rm para apagá-lo, a segunda camada apenas grava um marcador de ocultação (whiteout). Os 200 MB continuam gravados na camada anterior e viajam pela rede em todo docker pull.

  1. Limpe na mesma instrução. Encadeie instalação e limpeza de cache no mesmo comando RUN usando && (por exemplo, apt-get update && apt-get install -y ... && rm -rf /var/lib/apt/lists/*) para que os arquivos temporários nunca sejam congelados no snapshot da camada.
  2. Ordene do que menos muda para o que mais muda. O BuildKit reaproveita o cache de camadas enquanto as instruções e arquivos copiados forem idênticos. Assim que uma linha muda, todas as instruções seguintes perdem o cache. Copie primeiro o manifesto de dependências (go.mod, package.json ou requirements.txt), baixe os pacotes e só depois copie o código-fonte da aplicação, acompanhado de um arquivo .dockerignore que barre a pasta .git, segredos locais e artefatos de teste.
  3. Separe construção de execução com Multistage Builds. Use um primeiro estágio completo (FROM golang:1.21-alpine AS builder) para compilar e testar o código, e um segundo estágio mínimo (FROM scratch, alpine ou distroless) que usa COPY --from=builder para buscar apenas o binário pronto. No exemplo do livro, essa técnica reduz a imagem de um servidor web em Go de centenas de megabytes para cerca de 3,4 MB.
Comparação entre um Dockerfile ingênuo e um Dockerfile de produção
CritérioDockerfile de estágio único sem ordenaçãoDockerfile Multistage com BuildKit
Imagem base finalnode:latest ou golang:latest (tag mutável)Digest ou versão fixada + estágio final enxuto e usuário sem root
Impacto de mudar 1 linha de códigoReinstala todas as dependências do zero a cada buildReaproveita a camada de dependências e o --mount=type=cache
Superfície de ataque em produçãoInclui compiladores, curl, gerenciador de pacotes e shellApenas o executável necessário e certificados TLS
Comando para experimentar
# syntax=docker/dockerfile:1
FROM golang:1.21-alpine AS builder
WORKDIR /src
COPY go.mod go.sum* ./
RUN --mount=type=cache,target=/go/pkg/mod \
    go mod download
COPY . .
RUN --mount=type=cache,target=/root/.cache/go-build \
    CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /bin/app .

FROM alpine:3.19 AS release
RUN adduser -D -u 10001 appuser && apk --no-cache add ca-certificates
USER appuser
COPY --from=builder /bin/app /bin/app
EXPOSE 8080
ENTRYPOINT ["/bin/app"]

# Comandos no terminal para construir, inspecionar camadas e testar multiarquitetura:
# docker image build -t minha-api:1.0.0 .
# docker image history minha-api:1.0.0
# docker buildx build --platform linux/amd64,linux/arm64 -t minha-api:1.0.0 .

Inspecione cada camada antes de publicar

Execute docker image history na sua imagem recém-criada. O comando exibe o tamanho exato gerado por cada instrução do Dockerfile. Se você encontrar duas camadas grandes seguidas de instalação e limpeza separadas, junte-as imediatamente. Além disso, evite deixar credenciais em ARG ou ENV durante o build: qualquer pessoa com acesso à imagem consegue ler o histórico das instruções com docker image inspect.

Como depurar um erro de compilação usando a flag --target?

Quando a imagem final usa FROM scratch ou não possui shell, investigar por que um arquivo ficou faltando na compilação pode parecer difícil. Em vez de alterar o Dockerfile, execute docker image build --target builder -t debug-build . e abra um shell diretamente no estágio de construção com docker container run --rm -ti debug-build /bin/sh. Você inspeciona todas as ferramentas de build sem poluir a imagem que vai para produção.

Entrega da etapa

Pegue um Dockerfile existente (ou escreva um para sua aplicação) e crie um arquivo .dockerignore. Aplique a separação em dois estágios, adicione um usuário não-privilegiado (USER) e compare o tamanho final em megabytes e o tempo do segundo build consecutivo após alterar apenas uma linha de código.

Conexão com o livro · Capítulo 4 · empacotamento de aplicações, camadas do filesystem, cache do BuildKit, multistage builds e Buildx.

Oficina 03 / 08

Controle memória, CPU, sinais de parada e volumes

Um único contêiner sem teto de memória que sofre um vazamento sob pico de tráfego pode acionar o OOM Killer do kernel Linux e derrubar outros serviços que dividem a mesma máquina. Como definir fronteiras rígidas de hardware e armazenamento?

Ciclo de vida, sinais do Linux e cotas via cgroups

Quando você não informa limites ao comando docker container run, o processo dentro do contêiner pode disputar toda a memória RAM e todos os núcleos de CPU do servidor hospedeiro. Em produção, Kane e Matthias recomendam tratar limites de recursos como parte obrigatória do contrato de implantação:

Na memória, a flag --memory="256m" define um teto rígido no cgroup do contêiner, enquanto --memory-reservation="128m" estabelece um limite brando acionado quando o host sofre pressão de RAM. Se o processo ultrapassar o teto rígido e não houver espaço em swap (controlado por --memory-swap), o kernel Linux encerra o processo imediatamente e o Docker registra o código de saída 137 (128 + 9, referente ao sinal SIGKILL) com a flag OOMKilled: true no docker container inspect.

Na CPU, você pode limitar a fração de núcleos disponível com --cpus="1.5", definir pesos proporcionais de disputa com --cpu-shares=512 (que só atuam quando há contenção real de CPU no servidor) ou fixar o contêiner em núcleos específicos do processador com --cpuset-cpus="0,1" para evitar trocas de cache. Já no ciclo de vida, docker container stop envia primeiro o sinal SIGTERM ao processo principal (PID 1) e aguarda 10 segundos (ajustável com -t) para que a aplicação feche conexões antes de forçar SIGKILL. Para recarregar configurações sem derrubar conexões ativas (como no Nginx), use docker container kill --signal=USR1 ou HUP.

Blindagem do sistema de arquivos em execução

Comando para experimentar
# 1. Crie um volume gerenciado pelo Docker para dados persistentes
docker volume create dados-relatorios

# 2. Execute um contêiner com limites de RAM/CPU, init contra processos zumbis,
#    raiz somente leitura (--read-only) e /tmp efêmero em memória (tmpfs):
docker container run -d --name api-segura \
  --init \
  --restart on-failure:3 \
  --cpus="1.0" \
  --memory="256m" \
  --memory-swap="256m" \
  --read-only=true \
  --mount type=tmpfs,destination=/tmp,tmpfs-size=64m \
  --mount type=volume,source=dados-relatorios,destination=/var/lib/relatorios \
  nginx:1.25-alpine

# 3. Verifique os limites aplicados e se ocorreu morte por falta de memória (OOM):
docker container inspect --format '{{.State.Status}} | ExitCode={{.State.ExitCode}} | OOM={{.State.OOMKilled}}' api-segura

Note a combinação de três proteções no comando acima: --memory="256m" igual a --memory-swap="256m" impede que o contêiner use disco de swap lento quando a RAM acaba; --read-only=true bloqueia qualquer tentativa de alterar binários na camada do contêiner; e --mount type=tmpfs,destination=/tmp oferece uma pasta temporária rápida em RAM apenas onde a aplicação realmente precisa gravar arquivos transitórios.

Quando usar on-failure:3 em vez de unless-stopped?

A política --restart unless-stopped é excelente para serviços de longa duração que já foram estabilizados, pois recoloca o contêiner no ar mesmo após uma reinicialização do servidor físico. Porém, durante o lançamento de uma versão nova ou em tarefas em lote, um erro de configuração faz o contêiner cair em loop infinito de reinicialização, consumindo CPU e inundando os logs. Usar --restart on-failure:3 tenta reiniciar apenas se o código de saída for diferente de zero e para após três falhas consecutivas, preservando o estado para diagnóstico.

Entrega da etapa

Defina o envelope de recursos de um serviço real: indique a memória normal de operação, o teto rígido (--memory), a cota de CPU (--cpus), a política de reinicialização e quais pastas exigem escrita via volume ou tmpfs para que o restante do contêiner possa rodar com --read-only=true.

Conexão com o livro · Capítulo 5 · execução de contêineres, sinais, políticas de restart, cgroups de CPU/memória/I/O, volumes e sistemas de arquivos read-only.

Oficina 04 / 08

Configure logs sem travamento, métricas e testes reais de saúde

O contêiner aparece como “Up 4 hours” no painel, mas os clientes recebem erro de tempo esgotado porque o banco travou ou porque o coletor remoto de logs parou de responder e bloqueou a saída padrão da aplicação. Como enxergar a saúde real do serviço?

Rotação de logs, back-pressure e verificação ativa de saúde

Em ambientes conteinerizados, aplicações não devem gerenciar arquivos de log escondidos dentro do contêiner; elas escrevem eventos estruturados em STDOUT e STDERR para que o daemon do Docker encaminhe o fluxo ao driver configurado (como json-file, journald, syslog ou fluentd). Dois detalhes operacionais do Capítulo 6 evitam incidentes graves em produção:

Primeiro, o driver padrão json-file não limita o tamanho do arquivo em disco por padrão. Um serviço verboso pode encher a partição /var/lib/docker e derrubar o nó inteiro se você não definir max-size e max-file. Segundo, no modo padrão de entrega (mode=blocking), se o destino de log ficar lento para gravar, o Docker pausa a leitura do STDOUT do contêiner. Quando o buffer do sistema operacional enche, sua aplicação congela na chamada de escrita de log (fenômeno conhecido como back-pressure). Ativar mode=non-blocking com um max-buffer-size em memória protege a aplicação contra lentidões do sistema de log, ao custo de descartar mensagens antigas se o buffer lotar.

Modos de entrega de log do Docker e comportamento sob falha
ConfiguraçãoSe o disco ou servidor de log atrasarIndicação de uso
mode=blocking (padrão)Nunca descarta linhas de log, mas bloqueia a execução da aplicação quando o buffer encheTrilhas de auditoria financeira estrita onde nenhuma requisição pode prosseguir sem registro
mode=non-blocking + max-buffer-size=4mGuarda eventos no buffer circular em RAM e mantém a aplicação respondendo normalmenteAPIs web e microsserviços de baixa latência em produção
Comando para experimentar
# 1. Suba um serviço com verificação ativa de saúde (HEALTHCHECK) e logs rotacionados não-bloqueantes
docker container run -d --name web-monitorado \
  --log-driver json-file \
  --log-opt max-size=10m \
  --log-opt max-file=5 \
  --log-opt mode=non-blocking \
  --log-opt max-buffer-size=4m \
  --health-cmd="wget --no-verbose --tries=1 --spider http://localhost:80/ || exit 1" \
  --health-interval=15s \
  --health-timeout=3s \
  --health-start-period=10s \
  --health-retries=3 \
  nginx:1.25-alpine

# 2. Acompanhe o status de saúde (starting -> healthy -> unhealthy), métricas e eventos em tempo real:
docker container ps
docker container stats --no-stream web-monitorado
docker system events --since 10m --filter event=health_status

Observe a diferença entre verificar se o processo existe e verificar se ele atende requisições. Com HEALTHCHECK (no Dockerfile ou via flags de execução), o Docker executa o comando periodicamente dentro do contêiner: código de saída 0 marca o contêiner como healthy, enquanto falhas consecutivas igual a --health-retries mudam o estado para unhealthy e emitem um evento no docker system events que orquestradores usam para substituir a instância.

Como coletar métricas de todos os contêineres com Prometheus e cAdvisor?

O comando docker container stats é prático para diagnóstico rápido no terminal, mas em produção você precisa de séries históricas para cruzar picos de CPU, memória e rede com deploys. O próprio daemon do Docker pode expor métricas internas no formato Prometheus adicionando "metrics-addr": "127.0.0.1:9323" ao arquivo /etc/docker/daemon.json, enquanto o cAdvisor lê diretamente a hierarquia de cgroups do host para exportar consumo detalhado por contêiner.

Entrega da etapa

Escreva a instrução HEALTHCHECK ideal para o seu serviço (diferenciando o tempo de inicialização --start-period do timeout de resposta) e defina a política de logs (driver, tamanho máximo de rotação e decisão fundamentada entre blocking e non-blocking).

Conexão com o livro · Capítulo 6 · inspeção de contêineres, drivers de log, back-pressure, estatísticas de cgroups, cAdvisor, Prometheus e HEALTHCHECK.

Oficina 05 / 08

Diagnostique contêineres sem shell e dimensione o piloto de produção

Da investigação no kernel ao orçamento real do cluster

Lucas coordena a infraestrutura de uma plataforma de logística em Curitiba. Ao migrar o serviço de cálculo de frete para imagens enxutas (sem bash, sem ps e sem curl internos), a equipe reduziu o tamanho da imagem de 640 MB para 14 MB, mas ficou em dúvida sobre duas questões práticas: como investigar um travamento em produção se não há shell dentro do contêiner, e quantos pedidos o cluster piloto precisa processar para pagar a infraestrutura e o retrabalho de incidentes?

Inspecione processos, chamadas de sistema e arquivos pelo lado de fora

Como todo contêiner é apenas um conjunto de processos isolados por namespaces no kernel do host, você não precisa instalar ferramentas de diagnóstico dentro da imagem de produção. Kane e Matthias mostram quatro técnicas cirúrgicas do Capítulo 7:

Primeiro, execute docker container top nome-do-conteiner a partir do host para listar os processos em execução. Atenção a uma armadilha clássica: o comando traduz o UID numérico do contêiner usando o arquivo /etc/passwd do host, o que pode exibir o nome de outro usuário local que tenha o mesmo número de ID. Segundo, use docker container diff nome-do-conteiner para listar instantaneamente qualquer arquivo adicionado (A), modificado (C) ou apagado (D) desde que o contêiner subiu — ideal para flagrar logs esquecidos ou arquivos suspeitos. Terceiro, evite o acúmulo de processos zumbis (defunct) garantindo que o PID 1 trate sinais e recolha processos filhos órfãos com a flag --init (que injeta o tini). Quarto, quando precisar de strace, lsof ou tcpdump em um contêiner scratch, acople um contêiner lateral de diagnóstico compartilhando os namespaces de PID e de rede do contêiner-alvo.

Comando para experimentar
# 1. Inspecione os processos e arquivos alterados sem entrar no contêiner:
docker container top web-monitorado
docker container diff web-monitorado

# 2. Acople um contêiner de diagnóstico (netshoot/alpine) no MESMO namespace de PID e rede
#    de um contêiner enxuto que não possui shell nem ferramentas de rede:
docker container run --rm -it \
  --pid=container:web-monitorado \
  --net=container:web-monitorado \
  --cap-add sys_ptrace \
  alpine:3.19 /bin/sh -c "ps aux && netstat -tlpn"

Quando você passa --pid=container:web-monitorado e --net=container:web-monitorado, o contêiner efêmero de depuração enxerga exatamente os mesmos processos e portas abertas do contêiner principal, mas carrega suas próprias ferramentas no seu próprio sistema de arquivos e desaparece assim que você fecha a sessão.

Transforme a arquitetura em uma conta operacional transparente

Para validar a migração em produção, Lucas desenha um piloto de um mês atendendo clientes corporativos com SLA dedicado. Cada contrato mensal do módulo de cálculo de frete em tempo real custa R$ 120 por embarcador. Rodar a fatia de recursos de contêineres (CPU, RAM reservada, tráfego de registry e armazenamento de logs com rotação) somada às taxas variáveis consome R$ 45 por contrato. Preparar o pipeline automatizado, os alertas do Prometheus e o ambiente inicial custou R$ 600 fixos no mês do piloto. Além disso, cada incidente que exige rollback manual e reprocessamento de fila custa R$ 75 em horas técnicas e créditos de SLA.

Orçamento unitário por contrato no piloto de contêineres
Item operacionalValor mensal
Receita por contrato ativo no clusterR$ 120
Cota de CPU/RAM em contêineres e armazenamento de volumesR$ 24
Tráfego de rede, registry e retenção de logs rotacionadosR$ 9
Taxas variáveis e suporte de plantão estimado por contratoR$ 12
Margem de contribuição por contratoR$ 75

A conta unitária é 120 − (24 + 9 + 12) = R$ 75 por contrato (62,5% da receita). Como a preparação do ambiente e da automação custou R$ 600, são necessários 8 contratos (8 × 75 = R$ 600) para empatar o investimento inicial antes de qualquer incidente. Com 12 contratos ativos e 2 incidentes de rollback por OOM (2 × R$ 75 = R$ 150), o saldo do piloto fecha em 12 × 75 − 600 − 150 = R$ 150 positivos.

Sua decisão de engenharia e operação

Lucas havia definido três critérios para liberar a migração de 100% da carteira: pelo menos 12 contratos ativos no novo cluster, saldo operacional positivo e no máximo 1 incidente de rollback no mês. O piloto fechou com 12 contratos, R$ 150 de saldo e 2 rollbacks causados por código de saída 137 (OOM). Escreva em três linhas se você migraria toda a carteira agora ou qual ajuste faria antes.

Conferir uma decisão possível

A demanda e a eficiência dos contêineres foram comprovadas pelos 12 contratos e pelo saldo positivo de R$ 150, mas violar o limite de rollbacks por código 137 mostra que o dimensionamento de memória ou o controle de vazamento ainda não está seguro para receber toda a carteira. A decisão mais sólida é manter os 12 clientes atuais, inspecionar o consumo com docker container stats / cAdvisor, ajustar --memory e HEALTHCHECK e rodar mais uma quinzena sem código 137 antes de migrar o restante.

Três frentes práticas onde contêineres pagam o investimento

Use os cenários abaixo para conectar as técnicas das oficinas 01 a 05 ao tipo de projeto que você administra hoje.

Ambientes de desenvolvimento e CI idênticos à produção

Quando cada desenvolvedor instala versões diferentes do banco e da linguagem na própria máquina, a equipe gasta horas investigando erros que só acontecem no servidor de integração. Padronizar o ambiente em contêineres com versões fixadas elimina a divergência de bibliotecas e permite que qualquer pessoa nova na equipe suba a pilha inteira em minutos.

Consolidação segura de múltiplos serviços no mesmo servidor

Rodar três aplicações diretamente no mesmo servidor Linux costuma gerar conflitos de portas e bibliotecas compartilhadas. Isolar cada serviço em seu próprio contêiner com --cpus, --memory e --read-only permite aproveitar melhor a capacidade da máquina sem que um pico em um serviço secundário derrube a aplicação principal.

Jobs de processamento em lote e trabalhadores de fila efêmeros

Rotinas que convertem arquivos, geram relatórios ou processam filas se beneficiam diretamente da inicialização instantânea e da flag --rm: cada lote sobe em um contêiner limpo, sem lixo deixado pela execução anterior, e devolve toda a memória ao sistema operacional assim que termina.

Simule o saldo operacional do piloto em produção

Os valores iniciais reproduzem o piloto de Lucas. Altere um parâmetro por vez para testar como o custo de recursos por contêiner e o retrabalho de incidentes (OOM/rollback) impactam o ponto de equilíbrio.

Saldo = contratos × (receita − custo variável) − investimento inicial − incidentes × custo por incidente. O ponto de equilíbrio exibido considera o investimento inicial antes dos incidentes.

Conexão com o livro · Capítulos 5 e 7 · depuração de contêineres sem shell, nsenter, strace, docker container top/diff, PID 1 e dimensionamento operacional.

Oficina 06 / 08

Substitua scripts frágeis por orquestração local com Docker Compose

De comandos soltos a uma topologia declarativa verificável

No Capítulo 8, os autores mostram como equipes costumam começar automatizando múltiplos contêineres (como o servidor de chat Rocket.Chat, o banco MongoDB e o bot Hubot) com scripts shell cheios de sleep 15 e comandos docker run encadeados. O problema aparece na primeira falha: o script não sabe se o banco realmente terminou de inicializar, mistura redes e deixa contêineres órfãos quando uma variável de ambiente é esquecida. O Docker Compose resolve isso transformando a topologia inteira — serviços, redes isoladas, volumes e condições de prontidão — em um arquivo declarativo docker-compose.yaml.

Desenhe o ciclo de subida de uma pilha multicontêiner

  1. Isolar redes por camada. Em vez de colocar todos os contêineres na mesma rede padrão, declare uma rede frontend (onde ficam o proxy reverso e a API) e uma rede backend (onde ficam apenas a API e o banco de dados). Assim, o proxy nunca enxerga a porta do banco diretamente.
  2. Exigir prontidão real na dependência. Usar apenas depends_on: [db] garante que o contêiner do banco foi criado antes da API, mas não garante que o motor do PostgreSQL ou MongoDB já esteja aceitando conexões TCP. Combine um healthcheck no serviço do banco com condition: service_healthy no depends_on da API.
  3. Validar variáveis obrigatórias antes de subir. Use a sintaxe ${PORTA_API:-8080} para fornecer um valor padrão seguro e ${POSTGRES_PASSWORD:?Defina POSTGRES_PASSWORD no ambiente ou .env} para travar imediatamente com mensagem clara se um segredo obrigatório não tiver sido informado.
  4. Conferir o manifesto resolvido. Antes de iniciar os serviços com docker compose up -d, execute docker compose config. Ele valida a sintaxe YAML, aplica a precedência de variáveis (variáveis exportadas no shell vencem o arquivo .env) e imprime o manifesto final exato que será executado.

Experimente uma pilha completa com verificação de saúde

Leia o arquivo docker-compose.yaml abaixo e observe como o serviço api só inicia depois que o db passa no teste do pg_isready.

Comando para experimentar
# Salve como docker-compose.yaml:
services:
  db:
    image: postgres:16-alpine
    restart: unless-stopped
    environment:
      POSTGRES_USER: app
      POSTGRES_DB: pedidos
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?Informe a senha do banco em POSTGRES_PASSWORD}
    volumes:
      - pgdata:/var/lib/postgresql/data
    networks:
      - backend
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app -d pedidos"]
      interval: 5s
      timeout: 3s
      retries: 5

  api:
    image: nginx:1.25-alpine
    restart: unless-stopped
    ports:
      - "${PORTA_HTTP:-8080}:80"
    depends_on:
      db:
        condition: service_healthy
    networks:
      - frontend
      - backend

volumes:
  pgdata:

networks:
  frontend:
  backend:

# No terminal, valide a interpolação e suba o ambiente:
# POSTGRES_PASSWORD=segredo-local docker compose config
# POSTGRES_PASSWORD=segredo-local docker compose up -d
# docker compose ps && docker compose top
Comparar o que muda ao rodar docker compose down vs. docker compose down -v

O comando docker compose down para e remove os contêineres e as redes criadas pelo projeto, mas preserva os volumes nomeados (como pgdata) para que você não perca seu banco de dados ao atualizar a configuração de um serviço. Já docker compose down -v apaga também os volumes declarados — útil para zerar o ambiente entre baterias de testes automatizados, mas perigoso se executado sem atenção em uma máquina com dados importantes.

Compare a operação manual com a operação via Compose

Em um ensaio prático com uma equipe de seis pessoas mantendo uma aplicação de três contêineres (API, fila Redis e banco), compare o tempo e os erros de configuração ao preparar o ambiente local e de homologação.

Comparação prática na subida de um ambiente de 3 serviços
Indicador observadoScripts shell manuaisDocker Compose com healthcheck
Tempo médio para subir o ambiente do zero18 min2 min e 10 s
Erros de conexão da API por banco ainda iniciando4 em 10 subidas0 em 10 subidas
Variáveis ausentes detectadas antes de criar contêineresNão (falha silenciosa em runtime)Sim (via :? e docker compose config)
Limpeza completa de redes e contêineres órfãosManual sujeita a esquecimento1 comando (docker compose down)

Estruture o manifesto da sua aplicação

Escreva (ou revise) o docker-compose.yaml de um projeto com pelo menos dois serviços. Inclua separação de redes se houver mais de duas camadas, um volume nomeado para dados persistentes, validação :? para credenciais sensíveis e um bloco healthcheck que impeça a aplicação de subir antes do banco estar pronto.

Conexão com o livro · Capítulo 8 · orquestração local com Docker Compose, redes, volumes, interpolação de variáveis e dependências de saúde.

Oficina 07 / 08

Promova artefatos imutáveis e escale no Swarm e no Kubernetes

Se o pipeline de integração contínua testa uma imagem na homologação e, na hora de ir para produção, executa um novo docker build do zero ou puxa a tag :latest, você acabou de colocar em produção um artefato que nunca foi testado. Como garantir promoção confiável e atualização sem queda?

Um único artefato do commit até o cluster

Nos Capítulos 9 e 10, Kane e Matthias destacam a mudança mental mais importante ao levar contêineres para produção: construa a imagem uma única vez e promova exatamente o mesmo binário por todos os ambientes. Quando o desenvolvedor envia um commit para o repositório, o servidor de CI constrói a imagem, marca-a com o identificador imutável daquele commit (por exemplo, minha-api:git-9f3a1c2) e executa a bateria de testes contra ela. Se os testes passarem, o pipeline apenas adiciona uma tag de lançamento ou promove a referência pelo digest criptográfico (@sha256:...) para homologação e produção, injetando as configurações específicas de cada ambiente por fora.

Quando um único servidor já não basta — porque você precisa manter o sistema no ar mesmo que uma máquina física falhe — entram os orquestradores de cluster como Docker Swarm, Kubernetes e AWS ECS/Fargate. Eles recebem o estado desejado (por exemplo, “mantenha 4 réplicas saudáveis desta imagem com 256 MB de RAM cada”), distribuem os contêineres entre vários nós, roteiam o tráfego de entrada e substituem instâncias com falha automaticamente.

Comando para experimentar
# --- Opção A: Atualização gradual (Rolling Update) com rollback automático no Docker Swarm ---
docker swarm init
docker service create --name api-frete --replicas 3 -p 8080:80 \
  --update-parallelism 1 \
  --update-delay 10s \
  --update-order start-first \
  --update-failure-action rollback \
  nginx:1.24-alpine

# Atualize para a versão 1.25 subindo a réplica nova antes de parar a antiga:
docker service update --image nginx:1.25-alpine api-frete

# --- Opção B: Trecho essencial de um Deployment no Kubernetes (kubectl apply -f deploy.yaml) ---
# spec:
#   replicas: 3
#   strategy:
#     type: RollingUpdate
#     rollingUpdate:
#       maxUnavailable: 0
#       maxSurge: 1

Teste de raciocínio operacional em cluster

Uma equipe mantém um serviço com 6 réplicas distribuídas igualmente em 3 nós (2 réplicas por nó). Durante o horário comercial, o pico de tráfego exige pelo menos 5 réplicas ativas simultaneamente para não aumentar a latência. A equipe precisa colocar o Nó 3 em manutenção (drain) e, logo depois, aplicar uma atualização de versão. Resolva sem ajuda: o que acontece com a capacidade durante a drenagem do nó e qual parâmetro da estratégia de atualização evita cair abaixo de 5 réplicas?

Consultar uma pista

Quando você drena um nó que hospedava 2 das 6 réplicas, quantas réplicas permanecem atendendo tráfego nos primeiros segundos até que as novas instâncias passem no teste de prontidão (readinessProbe / HEALTHCHECK) nos outros dois nós?

Conferir a solução comentada

Se o serviço tinha 6 réplicas (2 por nó) e você drena o Nó 3 de uma só vez sem escalar antes, a capacidade cai momentaneamente para 4 réplicas (abaixo das 5 exigidas pelo pico) enquanto as 2 novas réplicas iniciam nos Nós 1 e 2. Para manter ao menos 5 réplicas o tempo todo, escale preventivamente para 7 ou 8 réplicas (ou configure --update-order start-first no Swarm / maxUnavailable: 0 e maxSurge: 1 no Kubernetes) garantindo que cada réplica nova esteja pronta antes de encerrar uma antiga.

Para testar a transferência do conceito, considere outro caso: um Deployment Kubernetes de 4 réplicas foi publicado com image: minha-api:latest. Duas réplicas subiram na segunda-feira no Nó A. Na quarta-feira, alguém publicou uma nova imagem sob a mesma tag :latest e o Nó B precisou recriar apenas uma réplica que havia reiniciado. Explique por que o cluster agora se comporta de forma inconsistente a cada requisição.

Conferir a transferência

Como a tag :latest é mutável, o Nó B baixou a imagem de quarta-feira para a réplica recriada, enquanto as réplicas que já rodavam no Nó A continuaram executando a imagem de segunda-feira em memória. Como o Service balanceia as requisições entre todos os Pods do Deployment, cerca de 25% dos clientes batem na versão nova e 75% na versão antiga. Fixar a tag imutável do commit (ou o digest @sha256) elimina essa divergência.

Entrega da etapa

Desenhe o fluxo de lançamento da sua aplicação em cinco passos: como a tag da imagem é gerada no CI, qual teste automático autoriza a promoção, como a atualização gradual sobe uma instância nova antes de desligar a antiga (start-first ou maxUnavailable: 0) e qual condição dispara o rollback automático.

Conexão com o livro · Capítulos 9 e 10 · contêineres em produção, pipelines CI/CD, Docker Swarm, Kubernetes (Pods, Deployments, Services, PVCs) e AWS ECS/Fargate.

Oficina 08 / 08

Blinde namespaces, capabilities do kernel e sua plataforma

Rodar um contêiner como root ou passar a flag --privileged=true para “resolver rápido” um erro de permissão entrega ao processo praticamente o controle total sobre o servidor hospedeiro. Como aplicar defesa em profundidade sem travar o trabalho da equipe?

Por dentro do runtime, dos namespaces e das capabilities do Linux

Nos Capítulos 11 e 12, o livro abre o capô do Docker para mostrar por que segurança de contêineres é segurança do kernel Linux. Quando o dockerd pede ao containerd que inicie um contêiner, um processo leve chamado containerd-shim-runc-v2 aciona o runc para configurar sete namespaces do Linux — mnt (pontos de montagem), uts (hostname), ipc (memória compartilhada), pid (numeração de processos), net (interfaces de rede e tabelas de roteamento), user (mapeamento de UIDs/GIDs) e cgroup (visão das cotas de hardware) — e em seguida o runc sai de cena, deixando apenas o shim como pai do contêiner. Isso permite inclusive habilitar "live-restore": true no daemon para atualizar o Docker sem derrubar os contêineres em execução.

O ponto crítico de segurança é que, salvo quando você ativa explicitamente o User Namespace Remapping ou o Rootless Mode, o usuário root (UID 0) dentro do contêiner é o mesmo UID 0 do kernel do host, apenas com parte dos poderes do kernel (as Linux Capabilities) desabilitados por padrão e filtrados por um perfil seccomp e políticas do AppArmor ou SELinux. Se alguém usar --privileged=true, todas as capabilities do kernel são devolvidas ao contêiner, incluindo o acesso direto aos dispositivos de disco do servidor hospedeiro.

Comando para experimentar
# Execute um contêiner aplicando o princípio do menor privilégio:
# 1. Remove TODAS as capabilities do kernel (--cap-drop=ALL)
# 2. Adiciona apenas a permissão estrita necessária (se precisar abrir porta < 1024)
# 3. Impede escalação de privilégio via binários setuid (no-new-privileges)
# 4. Roda com UID numérico sem privilégio e filesystem somente leitura
docker container run --rm -d --name api-blindada \
  --user 10001:10001 \
  --read-only=true \
  --mount type=tmpfs,destination=/tmp,tmpfs-size=32m \
  --cap-drop=ALL \
  --security-opt no-new-privileges:true \
  nginxinc/nginx-unprivileged:1.25-alpine

# Verifique as capabilities efetivas (CapEff deve ser 0000000000000000):
docker container exec api-blindada grep CapEff /proc/1/status

Verifique o resultado do comando grep CapEff /proc/1/status: quando todas as capabilities do kernel são removidas (0000000000000000) e o processo roda como usuário 10001 em um sistema de arquivos somente leitura, mesmo uma falha crítica na aplicação encontra uma parede no kernel.

Escolha o storage driver, a rede e o runtime corretos

O Capítulo 12 e o Capítulo 13 também orientam escolhas de infraestrutura que afetam o desempenho e o isolamento: no armazenamento, o driver overlay2 é o padrão recomendado para distribuições modernas (com suporte a btrfs e zfs em cargas específicas), lembrando que gravações intensas devem sempre ir para volumes para contornar o custo de copy-on-write. Na rede, além de bridge (com tradução NAT por porta), host e overlay (multihost criptografável com --opt encrypted), o driver macvlan entrega um endereço MAC próprio na rede física quando sistemas legados exigem presença direta na LAN. E, para cargas multi-inquilino de alto risco, você pode trocar o runtime padrão por gVisor (runsc, que intercepta syscalls em espaço de usuário) ou Kata Containers (que isola cada contêiner em uma micro-VM leve), além de usar ferramentas compatíveis com o padrão OCI como podman, buildah e nerdctl.

Projete aplicações prontas para contêineres

No encerramento do livro (Capítulo 14), os autores conectam o Docker às doze práticas do The Twelve-Factor App e aos quatro pilares do The Reactive Manifesto (sistemas responsivos, resilientes, elásticos e orientados a mensagens). Um contêiner bem blindado no kernel ainda falhará em produção se a aplicação demorar dois minutos para subir, guardar estado de sessão na memória local ou ignorar o sinal SIGTERM na hora de desligar.

Entrega da etapa

Faça uma auditoria de segurança e plataforma em um contêiner da sua equipe: verifique sob qual UID ele roda hoje, se funciona com --cap-drop=ALL e --security-opt no-new-privileges:true, se expõe o socket /var/run/docker.sock indevidamente e qual princípio do Twelve-Factor App ainda falta cumprir.

Conexão com o livro · Capítulos 11–14 · containerd, runc, namespaces, cgroups, rootless, capabilities, seccomp, storage/network drivers, gVisor/Kata e Twelve-Factor App.

Projeto de aplicação

Empacote, blinde e coloque um serviço em produção.

Escolha uma aplicação real do seu trabalho ou portfólio para conteinerizar do zero (ou refatorar para o padrão de produção). Sua entrega é uma especificação de implantação de uma página que qualquer pessoa da equipe consiga reproduzir e auditar.

Roteiro de uma semana

  1. Dia 1. Mapeie as fronteiras do serviço: separe o que entra na imagem imutável, o que vai para volumes ou bancos externos e o que será injetado por variáveis de ambiente.
  2. Dias 2 e 3. Escreva o .dockerignore e o Dockerfile multistage com cache do BuildKit e usuário não-privilegiado. Meça o tamanho da imagem antes e depois com docker image history.
  3. Dias 4 e 5. Suba a pilha completa no docker-compose.yaml com redes separadas, HEALTHCHECK ativo, limites de memória/CPU e logs rotacionados em modo non-blocking.
  4. Dia 6. Simule uma falha: mate o banco para testar a transição para unhealthy, verifique se --read-only=true e --cap-drop=ALL funcionam sem quebrar a aplicação e inspecione com um contêiner lateral.
  5. Dia 7. Documente a regra de versão imutável para o CI/CD, a política de atualização gradual (start-first), o gatilho de rollback e o responsável pela operação.

Decisões para praticar

Escolha uma resposta em cada situação de engenharia. A correção explica o funcionamento do Docker e permite tentar novamente.

1. Um Dockerfile executa "RUN curl -O arquivo-500mb.tar.gz" na linha 8 e "RUN rm arquivo-500mb.tar.gz" na linha 9. O que acontece com o tamanho final da imagem?
2. Um contêiner em produção encerrou subitamente com ExitCode 137 e OOMKilled=true no docker container inspect. Qual foi a causa imediata?
3. Você precisa inspecionar conexões e processos de um contêiner em produção construído sobre "FROM scratch" (sem shell, sem ps e sem netstat). Qual abordagem preserva a segurança da imagem?
4. Como garantir que o artefato colocado em produção seja exatamente o mesmo que passou nos testes automatizados do CI?

Minha especificação de produção

Antes de entregar para produção

Consultar anotações das oito oficinas
Suas anotações ficam neste navegador.
Livro-base e recursos técnicos

Sean P. Kane e Karl Matthias. Docker: Up & Running: Shipping Reliable Containers in Production, 3ª edição. O'Reilly Media, 2023. ISBN 978-1-098-13182-1. Os capítulos indicados em cada oficina referem-se à 3ª edição em inglês. O curso apresenta uma adaptação didática e prática em português brasileiro com estudos de caso, simulador operacional, exercícios e listas de verificação autorais. Página oficial do livro na O'Reilly · Repositório oficial de exemplos no GitHub.

Imagens editoriais geradas especialmente para a identidade visual do Ateliê da Tecnologia e incorporadas diretamente no documento para funcionamento offline.