guia de estudo · containers e docker em 13 etapas
Containers e Docker
pela lente da F1.
Este guia ensina, em 13 etapas, o caminho que uma aplicação percorre do notebook de quem a escreveu até um ambiente de produção, usando containers. Você começa entendendo por que containers existem e como o kernel Linux isola processos com namespaces e cgroups; depois separa imagem de container, escreve um Dockerfile, organiza camadas e cache e reduz o tamanho da imagem com bases mínimas e multistage build. Em seguida persiste dados com volumes, bind mounts e tmpfs, conecta serviços por nome em redes personalizadas e descreve a aplicação inteira em um arquivo do Docker Compose, com variações por ambiente, política de reinício, escala e uso em CI. Por fim, reduz privilégios (usuário não root, capabilities, seccomp, sistema de arquivos somente leitura), controla o que entra no build e como os segredos são tratados, investiga falhas com uma ordem de diagnóstico, verifica a prontidão real dos serviços com logs e healthchecks e junta tudo em uma stack pequena e defensiva. Antes das etapas, a seção seguinte apresenta a lente de F1 do guia: uma comparação geral que mostra o mesmo caminho, da fábrica ao box, funcionando dentro de uma equipe de Fórmula 1.
Cada etapa só depende do que foi explicado antes dela e termina com um exercício pequeno que prova que você entendeu. Containers se aprendem criando e destruindo coisas de propósito, então não pule os blocos "Mão na massa".
O que você precisa ter: Docker Engine ou Docker Desktop, com o comando docker compose (Compose v2) disponível, e um terminal com curl. Confirme com docker version e docker compose version. Os exemplos usam imagens Linux. No Docker Desktop, essas imagens rodam dentro da máquina virtual Linux administrada pela própria ferramenta. Alguns tutoriais antigos usam docker-compose (com hífen), o executável da primeira geração; neste guia usamos sempre docker compose.
Como usar este guia: mantenha um terminal aberto ao lado do texto e organize os arquivos em uma pasta por etapa (etapa01/, etapa02/ e assim por diante), porque nomes como Dockerfile e compose.yaml se repetem. Ao fim de cada etapa, remova os containers, redes e volumes de laboratório que ela criou, para que portas como 3000 e 8080 fiquem livres para a etapa seguinte. Quando um exemplo mostra a saída esperada, compare com a sua: IDs, tempos, tamanhos e versões vão variar, e a estrutura da saída é o que importa. Saídas abreviadas estão marcadas com ....
O que roda onde: todas as etapas rodam em Docker no seu computador. Só três exemplos dependem de algo externo: o docker push da Etapa 02 exige um registry real com permissão de escrita, o volume NFS da Etapa 05 exige um servidor NFS acessível, e as ferramentas Dive e Snyk da Etapa 10 precisam ser instaladas à parte (o Snyk também exige uma conta). Esses trechos são marcados no texto e o restante do guia não depende deles.
Sobre versões: imagens e linguagens evoluem. Os exemplos de Node.js usam as tags lts-alpine e lts-slim, que sempre apontam para a versão de suporte estendido vigente, para que os exercícios não envelheçam. Em um projeto real, a Etapa 02 explica por que fixar uma versão explícita é melhor.
A lente de F1 deste guia
O carro montado a partir da folha
Uma equipe de Fórmula 1 não fabrica o carro no circuito. Na fábrica, os engenheiros escrevem o manual de montagem, a sequência exata de passos: comece por este chassi (a estrutura central de fibra de carbono onde tudo é fixado), instale esta unidade de potência (o motor do carro, que junta o motor a combustão e a parte elétrica que recupera e devolve energia), depois o pacote aerodinâmico. A bancada de montagem recebe só uma caixa com as peças necessárias, e uma lista diz o que fica fora dela (anotações pessoais, chaves de acesso, sobras). Seguindo o manual, os mecânicos produzem um kit fechado: o carro em subconjuntos já montados, cada caixote de transporte com o resultado de um passo do manual aplicado sobre o anterior, e sem os moldes e as ferramentas, que ficam na fábrica. Se um passo e as suas peças são idênticos aos do último kit, a fábrica reaproveita o subconjunto pronto; se uma peça de baixo muda, tudo o que vai em cima dela é refeito, e por isso o que muda a cada fim de semana entra por último. O kit fica no armazém da equipe, de acesso restrito, e as peças padrão vêm de catálogos de fornecedores abertos a qualquer equipe. Cada kit leva uma etiqueta de versão ("especificação B"), que a equipe pode depois colar em outro kit, e uma lista de conteúdo com o número de série de cada peça, que muda se qualquer peça mudar. A logística despacha o mesmo kit para qualquer circuito, enviando só os caixotes que ainda não estão no destino. O que varia de um circuito para outro é a folha de parâmetros (mapas de motor, modos de energia), entregue à parte, sem trocar nenhuma peça.
No circuito, a equipe ocupa uma garagem no pit lane (a via dos boxes, onde ficam as garagens), dividida em um box para cada um dos dois carros que o regulamento permite. Os mecânicos tiram o carro dos caixotes e o põem em funcionamento no box, e ele está em operação enquanto o motor estiver ligado. O que se altera nele durante a sessão (um ajuste feito à mão, uma anotação no chassi) fica só naquele chassi e desaparece quando ele é desmontado. Divisórias separam os boxes: os mecânicos de um carro têm as suas ferramentas, a sua lista de pessoas e o seu canal de dados, e não enxergam o box ao lado. A energia, o ar comprimido e a rede de dados vêm da mesma instalação da garagem, e cada box tem um disjuntor que limita o quanto ele pode consumir, para que um box não deixe os outros sem energia. A alternativa seria erguer dentro da garagem uma estrutura completa, com gerador e compressor próprios: isola mais, mas pesa e demora para ficar pronta. O que precisa sobreviver ao chassi fica nos armários da garagem, ligados ao carro durante a sessão. Um engenheiro também pode ligar o carro direto a uma pasta do seu notebook, e o quadro-branco do box guarda o que é rápido e se apaga ao fim da sessão. Por dentro, carros e sistemas de apoio conversam pelo intercom, o rádio interno da equipe, em canais separados, e cada um é chamado pelo seu nome de rádio, nunca pela frequência, que pode mudar. Para fora, a garagem tem poucos pontos de contato, cada um ligado a um box; o que não tem ponto aberto só é alcançável pelo intercom.
Antes de chegar, a equipe escreve o plano do fim de semana: um documento único com cada sistema de que a garagem precisa (os carros, o servidor de telemetria, o banco de tempos), os canais e os armários de cada um, de modo que uma única ordem monte tudo. O plano define a ordem de montagem, e montado primeiro não quer dizer pronto. Uma folha complementar muda só o que é diferente num dia de testes, e o plano também fixa a regra para um sistema que desliga: religar sempre, religar só depois de defeito, com um número máximo de tentativas, ou respeitar quem desligou de propósito. Na fábrica, um banco de testes automático monta o conjunto a cada peça nova, roda os testes e desmonta. Cada pessoa recebe uma credencial que abre só as áreas de que precisa, nunca o passe que abre tudo, e há pedidos que nenhum box pode fazer à instalação da garagem, como desligar o quadro geral de energia ou ligar um equipamento novo direto na rede elétrica: a instalação recusa esses pedidos, venham de quem vierem. A partir da classificação (a sessão que define a ordem de largada), o carro fica em parc fermé, o regime do regulamento em que ele fica lacrado e não pode ser modificado, com intervenções só em pontos permitidos. Códigos de acesso chegam em envelope, à pessoa certa; um código escrito numa peça durante a montagem continua registrado no kit mesmo que um passo seguinte o cubra. Durante a sessão, cada carro envia telemetria (os dados dos sensores, transmitidos continuamente) num formato fixo para os servidores da equipe. O engenheiro de corrida (quem acompanha um carro) faz de tempos em tempos o radio check, a chamada de rotina pelo rádio que confirma que o carro responde e consegue seguir o plano, o que diz mais do que ver o motor ligado; na volta de saída (a primeira, com pneus e freios ainda frios), uma resposta ruim ainda não conta. Quando um carro para, a investigação segue uma ordem: o estado e o motivo registrado da parada, os dados gravados pelo próprio carro, a ficha do que foi montado, a linha do tempo da garagem, o consumo e, só então, um notebook ligado ao carro.
É a mesma cadeia que o Docker percorre. A garagem é o host e a instalação comum é o kernel compartilhado; as divisórias entre os boxes são os namespaces e o disjuntor de cada box é o cgroup. O kit fechado é a imagem, e o carro em funcionamento no box é o container. O manual de montagem é o Dockerfile, e cada caixote do kit é uma camada da imagem. Os armários da garagem são os volumes. Cada canal do intercom é uma rede personalizada, e o nome de rádio é o DNS interno. O plano do fim de semana é o arquivo do Docker Compose. A credencial que abre só algumas áreas e o parc fermé são o menor privilégio aplicado ao container, e o envelope é o secret. O radio check é o healthcheck. Uma regra fixa vale para o guia inteiro: cada carro tirado dos caixotes e posto em funcionamento num box é um container. O kit no armazém nunca é alterado, e quem quer um carro diferente muda o manual e produz outro kit.
Se você já leu os guias de Kubernetes: aqui o guia olha uma garagem só, sem a mureta (a bancada voltada para a pista, onde ficam os engenheiros que decidem), e você dá cada ordem diretamente com o comando docker. Nos guias de Kubernetes, o carro passa a ser o Pod, e o container vira um dos sistemas dentro dele.
Os termos em palavras simples
54
O quadro abaixo traduz cada termo técnico do guia para uma ideia do dia a dia, sem F1 e sem jargão. Não precisa decorar; volte a ele quando um termo aparecer no texto.
| Termo técnico | Em palavras simples |
|---|---|
| Container | Um quarto de hotel ocupado por um programa: ele usa só o que está dentro do quarto e não enxerga os outros quartos, mas o prédio (encanamento, energia) é compartilhado. |
| Kernel | A central do prédio que fornece energia, água e portaria para todos os quartos ao mesmo tempo. Todos os containers de um computador usam a mesma. |
| Host | O computador (ou servidor) real onde os containers funcionam, ou seja, o prédio inteiro. |
| Máquina virtual (VM) | Uma casa inteira e independente, com instalação elétrica e hidráulica própria, construída dentro do prédio. Isola bem, mas é pesada e demora mais para ficar pronta que um quarto. |
| Namespace | As paredes do quarto: definem o que cada programa consegue ver (quais processos, qual rede, quais arquivos, qual nome). |
| Cgroup | O medidor com limite em cada quarto: define quanto de processamento, memória e disco aquele programa pode gastar. |
| Processo principal (PID 1) | O primeiro programa que o container inicia, o responsável pelo quarto: quando ele sai, o quarto é fechado. Como é ele quem recebe os avisos do prédio (como "pare agora"), precisa saber reagir a eles; um "init" é um recepcionista que ocupa essa vaga e repassa os avisos ao programa. |
| Imagem | O quarto-modelo pronto e lacrado, montado seguindo a receita: planta, móveis e tudo o que o quarto precisa. Cada quarto de verdade (container) é uma cópia dele, e mexer em um quarto não altera o modelo. |
| Registry e Docker Hub | Uma biblioteca de quartos-modelo na internet, da qual você baixa os que precisa. O Docker Hub é a biblioteca pública mais conhecida; também existem bibliotecas privadas, só para quem tem senha. |
| Tag | A etiqueta de versão colada no modelo, como "edição 2". Quem etiqueta pode, mais tarde, mudar a etiqueta de um modelo para outro. |
| Digest | O número de série impresso do conteúdo exato do modelo. Se um detalhe mudar, o número muda, e por isso ele identifica sem ambiguidade. |
| Camada gravável | O bloco de notas do quarto. As anotações continuam lá quando o hóspede sai e volta (parar e reiniciar o container) e só somem quando o quarto é desmontado, isto é, quando o container é removido. |
| Dockerfile | A receita escrita: a lista de passos para montar o quarto-modelo ("comece com isto, adicione aquilo, rode este comando"), para que qualquer pessoa repita o mesmo resultado. |
| Build | O ato de seguir a receita do Dockerfile, passo a passo, para produzir o quarto-modelo. |
| EXPOSE | Uma placa na porta do quarto dizendo "atendo na porta 3000". A placa informa, mas não abre a porta para o público; quem abre é a publicação de porta. |
| Camada de imagem | Folhas transparentes empilhadas: cada passo da receita acrescenta uma folha por cima, e as folhas de baixo não mudam. |
| Cache de build | Se um passo da receita e os materiais dele são idênticos aos da última vez, o Docker reaproveita o trecho que já estava pronto em vez de refazer. |
| Imagem base (alpine, slim, distroless) | O ponto de partida da receita. Alpine é um quarto-base minúsculo, com poucos móveis; slim é um quarto reduzido, mas familiar; distroless tem só o essencial do hóspede e nenhum móvel de apoio. |
| Multistage build | Montar o quarto numa oficina cheia de ferramentas e entregar só o quarto pronto, sem as ferramentas. |
| Volume | Um armário guardado fora do quarto: o quarto pode ser trocado, e o armário continua com tudo dentro. |
| Bind mount | Uma porta que abre direto numa pasta do seu computador, de modo que o que muda em um lado aparece no outro. |
| tmpfs | Um quadro-branco: rápido, mas apagado quando a reunião termina. Fica na memória e, em geral, não é gravado em disco. |
| Rede personalizada e DNS interno | Um grupo de mensagens só com os integrantes do projeto, em que cada um é chamado pelo nome e não pelo número de telefone, que pode mudar. |
| Publicação de porta | Abrir uma janela de atendimento na fachada do prédio para o público. As outras salas continuam internas. |
| Docker Compose | Um roteiro único que lista todos os quartos do andar, como se ligam e o que cada um precisa, para montar tudo com um só pedido. |
| Serviço | Cada item do roteiro do Compose, por exemplo "a API" ou "o banco". |
| depends_on | A ordem de abertura dos quartos: "abra o do banco antes do da API". Garante só a ordem de abertura, e não que o banco já esteja pronto para atender, a menos que haja uma verificação de saúde combinada. |
| Override | Uma folha de ajustes sobreposta ao roteiro: muda só o que é diferente em um ambiente (por exemplo, abrir uma porta só no seu computador) sem reescrever o roteiro inteiro. |
| Política de reinício | A regra sobre o que fazer quando o programa para: reabrir sempre, reabrir só se parou por defeito, ou respeitar quando foi você quem mandou parar. |
| Réplica (escala) | Uma cópia idêntica de um serviço, como abrir mais um guichê igual para atender mais gente. |
| CI (integração contínua) | Um ajudante automático que, a cada mudança no projeto, monta o ambiente, roda os testes e desmonta tudo. |
| Usuário não root | Dar ao programa um crachá comum em vez da chave-mestra do prédio. Se o crachá for roubado, o estrago é bem menor. |
| Capabilities | A chave-mestra dividida em dezenas de chaves pequenas (abrir a porta A, ligar o gerador). Cada programa recebe só as chaves de que precisa. |
| Seccomp | Uma lista de pedidos que o programa pode ou não fazer à central do prédio; os pedidos perigosos são recusados. |
| Menor privilégio | A regra de dar a cada um somente o acesso necessário para a tarefa, e nada além. |
| Sistema de arquivos somente leitura | Um documento com proteção contra gravação: dá para ler, mas ninguém consegue alterar. |
| Contexto de build e .dockerignore | O contexto é a caixa de materiais enviada para quem monta o quarto-modelo. O .dockerignore é a lista do que fica de fora da caixa (rascunhos, senhas, lixo). |
| Segredo (secret) | Uma senha entregue em mãos, num envelope, em vez de escrita num cartaz na porta. |
| Variável de ambiente | Um bilhete fixado no quarto do programa: ele lê quando precisa, mas qualquer pessoa que inspecione o quarto também consegue ler. |
| ARG | Um parâmetro dado na hora de seguir a receita (como o número da versão). Fica registrado na história da montagem, então não serve para senha. |
| Vulnerabilidade (CVE) | Um defeito de segurança já catalogado e com número de registro, como o recall de um produto. |
| Dive e Snyk | Duas lupas: uma mostra o que cada folha da imagem acrescentou e quanto pesa; a outra procura defeitos de segurança já conhecidos. |
| Código de saída | O número que um programa deixa ao terminar: 0 significa "terminei bem" e qualquer outro número indica que algo deu errado. |
| OOMKilled e código 137 | OOMKilled é o registro de que o container foi encerrado por pedir mais memória do que o permitido (OOM quer dizer "sem memória"). O 137 é o número deixado por um encerramento à força; isso acontece por falta de memória, mas também quando um pedido educado de parada não é atendido a tempo. |
| Inspect | Pedir a ficha técnica completa de um container: o que ele executa, o que está montado, em qual rede está e como terminou. |
| Evento | A linha no diário do Docker que registra cada acontecimento (criado, iniciado, parado, removido), com a hora. |
| Log (stdout e stderr) | O diário que o programa escreve sozinho enquanto trabalha. Stdout recebe as mensagens normais e stderr recebe os erros. |
| Log estruturado | Diário em que cada anotação segue sempre o mesmo formulário (hora, nível, mensagem), o que permite buscar e filtrar com facilidade. |
| Healthcheck | Perguntar de tempos em tempos "você consegue atender agora?", em vez de apenas olhar se a luz do balcão está acesa. |
| start_period | O tempo de carência logo depois que o container sobe: nesse período, as respostas negativas não contam como falha, porque o programa ainda está se acomodando. |
| Prontidão | Estar realmente apto a atender, e não só ligado. |
| Stack | O conjunto completo de peças que formam uma aplicação funcionando (API, banco, rede, armazenamento). |
| Limite de recursos | Um teto de consumo: o programa pode usar até tanto de memória e processamento, mas isso não garante que ele terá esse tanto reservado. |
| Label | Uma etiqueta informativa colada no container, usada para organizar e buscar. Ela descreve e não protege nada. |
Onde a comparação para
13
A comparação ajuda a fixar o caminho da fábrica até o box, mas não é uma equivalência perfeita. Estes são os pontos em que a F1 deixa de valer para cada etapa:
- Etapa 01: o carro é uma máquina completa e as divisórias são paredes físicas; o container é só um processo comum do host, que o kernel enxerga e pode encerrar como qualquer outro, e o isolamento é uma configuração desse mesmo kernel, não uma barreira física. Uma falha no kernel compartilhado atinge todos os containers ao mesmo tempo.
- Etapa 02: um kit físico vira um carro só, e montar consome as peças; uma imagem gera quantos containers forem pedidos, ao mesmo tempo, sem se gastar, e remover os containers não remove a imagem. O digest, além disso, é calculado a partir do próprio conteúdo por uma função de hash, e não atribuído por alguém como um número de série.
- Etapa 03: o manual de montagem é lido por mecânicos que resolvem ambiguidades; o Dockerfile é executado ao pé da letra, só enxerga os arquivos do contexto de build, e o
EXPOSEapenas documenta a porta, sem abrir nenhum ponto de contato para fora (a porta publicada). - Etapa 04: na fábrica, decidir se um subconjunto pronto (uma camada em cache) pode ser reaproveitado é um julgamento de engenharia; no build, o cache depende de comparação exata da instrução e dos arquivos de entrada, e qualquer diferença invalida aquela camada e todas as seguintes, mesmo sem efeito prático. E apagar um arquivo num passo posterior não deixa o kit mais leve: a camada que o adicionou continua na imagem.
- Etapa 05: os armários são móveis separados do carro; o volume é um diretório do host montado dentro do container, e o bind mount dá ao container acesso de escrita a uma pasta real do host, com as permissões dela, sem paralelo seguro na garagem. Um volume NFS remoto depende ainda da rede e das permissões do servidor, e não só do Docker.
- Etapa 06: no intercom, todos no canal ouvem todas as mensagens; numa rede Docker, cada container tem IP próprio e as conexões são ponto a ponto. Só as redes personalizadas resolvem nomes (a bridge padrão não resolve), e o modo
hostelimina o isolamento de rede, sem equivalente num canal de rádio. - Etapa 07: o plano do fim de semana é executado por pessoas que esperam um sistema ficar pronto antes de montar o seguinte; o
depends_onsimples só garante a ordem de criação, e o Compose só espera a prontidão quando há healthcheck e a condiçãoservice_healthydeclarada. - Etapa 08: o regulamento fixa dois carros; o
--scalecria quantas réplicas idênticas forem pedidas, limitado pelo host e pela porta fixa publicada, que só uma réplica consegue ocupar. E a regra de religar (a política de reinício) reage só ao fim do processo: um sistema que continua ligado sem responder não é religado por ela. - Etapa 09: no parc fermé (a raiz somente leitura), pessoas conferem os lacres e julgam exceções; no container,
--read-only,--cap-drope seccomp são aplicados pelo kernel em cada operação, sem margem de interpretação. Além disso, por padrão o container roda como root, o que equivale a entregar o passe que abre tudo (o usuário root) até alguém configurar outro usuário, e montar odocker.socknum container dá a ele o comando de todos os boxes da garagem. - Etapa 10: um envelope entregue em mãos só existe para quem o recebeu; um secret do Compose é um arquivo em
/run/secretsque qualquer processo do container com permissão de leitura enxerga. E um código que entrou numa camada da imagem fica no histórico mesmo coberto por um passo seguinte, e só some quando a imagem é reconstruída sem ele. - Etapa 11: um engenheiro pode ligar um notebook (o
docker exec) a um carro parado na garagem; odocker execsó funciona com o container em execução, e um container encerrado deixa apenas estado, código de saída, logs e metadados. O disjuntor de cada box corta tudo de uma vez, enquanto o cgroup trata os recursos de modo diferente: excesso de CPU só deixa o processo mais lento, e é o excesso de memória que leva ao encerramento marcado comoOOMKilled. - Etapa 12: o radio check é feito por uma pessoa que entende a resposta; o healthcheck executa um comando, olha só o código de saída e prova apenas o que o teste examina. Sem orquestrador, o estado
unhealthyfica registrado, mas o Docker não religa nem substitui o container por causa dele. E a telemetria (os logs) vai sempre para os servidores da equipe, enquanto odocker logslê o que ficou no host e os logs costumam se perder com a remoção do container, a menos que sejam centralizados. - Etapa 13: na garagem, pessoas compensam um item esquecido no plano do fim de semana (o arquivo Compose); na stack, cada proteção só vale se estiver declarada no arquivo. Os limites de CPU e memória funcionam como o disjuntor de cada box, contendo o consumo sem reservar capacidade para o serviço, e as labels são etiquetas de inventário, como as dos caixotes, e não lacres: descrevem e não protegem nada.
Etapa 01 fundamentos
Por que containers existem
O problema
Uma aplicação Node.js funciona no notebook da equipe, mas falha no servidor. A máquina local usa uma versão da linguagem, o servidor usa outra, uma biblioteca nativa está ausente e uma configuração foi feita à mão, sem documentação. O deploy vira uma tentativa de reconstruir, passo a passo, um ambiente que só existe na memória de quem o configurou.
O mesmo desgaste aparece em outras situações. Instalar pacotes e ajustar permissões direto no servidor é lento e difícil de reproduzir, e qualquer mudança pequena pode quebrar tudo. Subir uma segunda instância da aplicação exige copiar e configurar tudo de novo em outra máquina. E, se a versão nova dá problema, voltar para a anterior significa reconfigurar tudo ou torcer para existir um backup funcional.
As máquinas virtuais (VMs) melhoraram o isolamento desse cenário, mas cada VM carrega seu próprio sistema operacional completo. Isso custa memória, disco e tempo de inicialização. Quando o objetivo é executar aplicações isoladas no mesmo servidor, repetir um sistema operacional inteiro para cada uma pode ser desperdício.
O conceito
A evolução foi esta: primeiro, uma aplicação por servidor físico, com configuração manual; depois, a virtualização, que divide um servidor em várias VMs; por fim, os containers, que isolam apenas os processos e compartilham o kernel do sistema operacional do host. O foco deixa de ser a máquina e passa a ser a aplicação.
Um container é um conjunto de processos isolados no nível do sistema operacional. Em Linux, dois mecanismos do kernel tornam isso possível:
- Namespaces controlam o que o processo consegue enxergar. Há um tipo para cada recurso: PID (a lista de processos), rede (interfaces e tabelas de rota), mount (sistema de arquivos), UTS (hostname), IPC (comunicação entre processos, como filas e memória compartilhada) e user (identificação de usuários e grupos).
- Cgroups (control groups) medem e limitam recursos, como CPU, memória e operações de entrada e saída em disco. Um container que tenta consumir mais que o previsto fica limitado ao que foi definido, e isso protege os demais.
O container compartilha o kernel do host. Por isso ele costuma iniciar mais rápido e consumir menos recursos que uma VM. Esse compartilhamento também significa que container não é sinônimo de máquina virtual nem uma barreira absoluta de segurança: a proteção depende da configuração do runtime e do host (a Etapa 09 aprofunda isso).
O Docker é a ferramenta que tornou containers acessíveis: ele padroniza como empacotar, distribuir e executar. Uma aplicação empacotada roda do mesmo jeito no seu notebook, no servidor de teste, em produção ou na nuvem, o que elimina o problema do "na minha máquina funciona".
Os conceitos-chave
Container
processos isolados e limitados pelo kernel; ele vive somente enquanto o seu processo principal estiver ativo.
Kernel compartilhado
é a diferença central em relação a uma VM e a razão de os containers serem leves.
Namespace
isola a visão de processos, rede, arquivos, hostname, comunicação entre processos e usuários.
Cgroup
mede e limita CPU, memória e entrada e saída de disco.
VM
simula um computador completo, com kernel próprio; isola mais, mas custa mais.
Docker
ferramenta que cria, empacota e executa containers de forma padronizada.
Exemplos comentados
# Baixa a imagem, cria um container, executa seu processo e exibe a saída.$ docker run --name primeiro-container hello-world# Lista também containers que já terminaram.$ docker ps --all# Mostra metadados completos do container em JSON.$ docker inspect primeiro-container# Remove o container já encerrado.$ docker rm primeiro-container# saída esperadaHello from Docker!...CONTAINER ID IMAGE STATUS NAMESabc123 hello-world Exited (0) ... primeiro-container
O status Exited (0) não indica falha. O processo principal imprimiu a mensagem e terminou com sucesso. Um container permanece em execução somente enquanto seu processo principal permanece ativo.
Comandos essenciais desta etapa
docker ps -a): lista também os containers paradosMão na massa
- Execute
docker run --name fundamentos hello-world. - Rode
docker pse depoisdocker ps --all. - Observe que o container aparece apenas no segundo comando.
- Inspecione seu status com
docker inspect --format '{{.State.Status}} {{.State.ExitCode}}' fundamentos. - A saída esperada é
exited 0. Remova-o comdocker rm fundamentos. - Rode
docker run --rm alpine pse observe que o único processo listado tem PID 1: o container não enxerga os processos do host. - Rode
docker run --rm --hostname demo alpine hostnamee depoisdocker run --rm alpine hostname. Observe que o nome muda de um container para outro, e nenhum deles é o nome do seu computador.
Dica: Quando um container "some" logo depois de docker run, não suponha defeito: rode docker ps --all e leia o STATUS. Um Exited (0) significa que o processo principal terminou normalmente, e um container só continua vivo enquanto esse processo continuar.
Anota aí: Container é um processo isolado e limitado pelo kernel; ele não é uma máquina virtual pequena.
Etapa 02 imagem + runtime
Imagem, container e ciclo de vida
O problema
Uma equipe precisa executar a mesma versão do Nginx em desenvolvimento, teste e produção. Instalar e configurar o programa manualmente em cada ambiente recria o problema de inconsistência. Também não basta copiar um container em execução, porque ele acumula estado temporário e não representa uma receita reproduzível.
O conceito
Uma imagem reúne o sistema de arquivos e os metadados necessários para iniciar um processo. Ela é imutável e composta por camadas reutilizáveis. Um container acrescenta uma camada gravável temporária sobre a imagem e executa o comando configurado. A mesma imagem pode originar vários containers independentes, e remover um container não remove a imagem.
O fluxo básico é:
docker pullbaixa uma imagem de um registry, como o Docker Hub.docker runcria e inicia um container a partir da imagem.docker stopsolicita a parada do processo.docker startreinicia o mesmo container.docker rmremove o container.docker rmiremove uma imagem que não está sendo usada.
Tags como node:lts-alpine ou nginx:alpine identificam variantes. Evite depender de latest em ambientes que precisam de reprodutibilidade, pois o conteúdo associado à tag pode mudar. Uma tag explícita (por exemplo, a versão principal) ajuda, embora uma tag ainda possa ser movida pelo mantenedor. Quando a exigência for imutabilidade forte, um digest (imagem@sha256:...) identifica o conteúdo da imagem com precisão.
Um registry público permite descobrir e baixar imagens públicas, muitas vezes sem autenticação. O Docker Hub é o exemplo mais conhecido e hospeda tanto software livre (Nginx, Node.js) quanto bases de sistema (Ubuntu, Alpine). Um registry privado restringe o acesso a imagens internas e exige autenticação com docker login. Empresas costumam combinar os dois: imagens comuns vêm do registry público, e as imagens que contêm o código proprietário ficam em repositórios internos, acessíveis apenas a quem é autorizado. Entre as opções de registry privado estão o Amazon Elastic Container Registry (ECR), o Azure Container Registry (ACR), o Google Artifact Registry (que substituiu o antigo Google Container Registry), e soluções auto-hospedadas como Harbor e JFrog Artifactory. Elas se integram a pipelines de CI/CD para automatizar a criação e a distribuição das imagens.
Use imagens públicas apenas quando forem confiáveis e verificadas, e guarde em repositórios privados tudo o que for sensível ou interno.
Antes de enviar uma imagem, o nome local precisa receber uma tag que inclua o endereço do registry, o namespace e a versão.

Um container é a imagem, em camadas somente leitura, mais uma camada gravável fina por cima. Tudo o que o container escreve vai para essa camada.

Vários containers da mesma imagem compartilham as camadas somente leitura, e cada um tem a sua camada gravável. Remover um container apaga só a camada dele.
Os conceitos-chave
Imagem
modelo imutável, em camadas, usado para criar containers.
Container
execução de uma imagem, com uma camada gravável temporária por cima.
Camada gravável
onde o container grava durante a execução; ela desaparece quando o container é removido.
Registry
servidor que armazena e distribui imagens (Docker Hub, ECR, ACR, Artifact Registry, Harbor).
Tag
nome de versão de uma imagem; é mutável, e latest não garante reprodutibilidade.
Digest
identificador do conteúdo exato da imagem.
Ciclo de vida
pull, run, stop, start, rm, rmi.
Exemplos comentados
# Baixa explicitamente a variante Alpine da imagem oficial.$ docker pull nginx:alpine# Cria um container em segundo plano.# O nome facilita os próximos comandos.# A porta 8080 do host é encaminhada para a porta 80 do container.$ docker run --detach --name pagina --publish 8080:80 nginx:alpine# Lista os containers em execução.$ docker ps# Consulta o servidor dentro do container pela porta publicada no host.$ curl http://localhost:8080# Mostra as mensagens produzidas pelo processo do Nginx.$ docker logs pagina# Executa um comando em um container que já está em execução.$ docker exec pagina nginx -v# Encerra e remove o container.$ docker stop pagina$ docker rm pagina# saída esperadaCONTAINER ID IMAGE PORTS NAMESabc123 nginx:alpine 0.0.0.0:8080->80/tcp pagina<!DOCTYPE html><title>Welcome to nginx!</title>nginx version: nginx/...
Comandos essenciais desta etapa
docker run -it ubuntu bash)docker exec -it meu-container bash)docker tag, docker push: autentica, nomeia e envia uma imagem a um registrydocker image prune, docker system prune: removem containers parados, imagens sem uso e todos os recursos não utilizados, respectivamente. Leia a lista que o Docker apresenta e confirme o escopo antes de aceitarMão na massa
- Suba
nginx:alpinena porta 8080 com o nomeciclo. - Confirme a resposta com
curl -I http://localhost:8080. A primeira linha da saída deve serHTTP/1.1 200 OK. - Pare o container e repita o
curl. A conexão deve falhar, com uma mensagem comocurl: (7) Failed to connect to localhost port 8080. - Execute
docker start cicloe confirme que a página voltou (HTTP/1.1 200 OKde novo). - Confirme que não há dado necessário somente dentro do container. Então remova-o com
docker rm --force ciclo. Essa opção interrompe e remove a instância imediatamente, por isso não deve ser usada sem conferir o alvo. - Rode
docker images nginxe observe que a imagem continua disponível mesmo sem nenhum container: remover o container não remove a imagem. - Garanta que o arquivo
nota.txtexiste na pasta atual (echo "ola do host" > nota.txt). Crie um container comdocker run --detach --name caixa alpine sleep 300, copie o arquivo para ele comdocker cp nota.txt caixa:/tmp/nota.txt, leia-o comdocker exec caixa cat /tmp/nota.txt(a saída éola do host) e remova o container comdocker rm --force caixa. Em seguida crie outro com o mesmodocker rune rode de novodocker exec caixa cat /tmp/nota.txt. A saída esperada agora écat: can't open '/tmp/nota.txt': No such file or directory, porque o arquivo ficou na camada gravável do container que foi removido. Remova o segundo container comdocker rm --force caixa.
Dica: Quando precisar reproduzir exatamente uma imagem, anote o digest com docker images --digests e referencie imagem@sha256:.... Tags como latest e até stable mudam de conteúdo com o tempo; digests não.
Anota aí: Imagem é o artefato reproduzível; container é uma execução descartável dessa imagem.
Etapa 03 build reproduzível
Dockerfile como receita reproduzível
O problema
Baixar uma imagem pronta resolve ferramentas comuns, mas não empacota o código da sua aplicação. Copiar arquivos manualmente para um container em execução produz um ambiente impossível de reconstruir com confiança.
O conceito
O Dockerfile é um arquivo de texto que descreve, de forma declarativa, o passo a passo para construir uma imagem: você diz o que quer e o Docker constrói. O comando docker build lê o Dockerfile e uma pasta de arquivos (o contexto de build, indicado no final do comando, normalmente .) e produz uma imagem.
As instruções essenciais são:
FROM: escolhe a imagem base. Exemplo:FROM node:lts-alpinesignifica "comece a partir da imagem do Node em uma base Alpine".WORKDIR: define o diretório usado pelas instruções seguintes, para evitar caminhos longos em cada comando. Exemplo:WORKDIR /app.COPY: copia arquivos do contexto de build para dentro da imagem. Exemplo:COPY . /appcopia tudo o que está no diretório atual para/app.RUN: executa uma ação durante a construção (instalar dependências, criar pastas) e grava o resultado como uma nova camada. Exemplo:RUN npm install.CMD: define o comando padrão executado quando o container inicia. Exemplo:CMD ["node", "index.js"].EXPOSE: documenta a porta usada pela aplicação. Ela não publica a porta no host.
RUN acontece no build e grava uma camada na imagem. CMD acontece no runtime, quando um container é iniciado. Misturar esses dois momentos é uma fonte comum de erro. O CMD pode ser substituído na linha de comando do docker run.
A ordem das instruções importa porque o Docker guarda em cache o resultado de cada uma. Se você muda algo no topo, tudo o que vem abaixo é reconstruído; se muda apenas o CMD, o restante é reaproveitado. A Etapa 04 explora essa regra.
O processo principal e os sinais. O processo definido pelo CMD é o PID 1 do container. No Linux, o PID 1 não recebe o tratamento padrão de sinais: um sinal como SIGTERM ou SIGINT, que normalmente encerra um programa, é ignorado se o programa não instalou um tratador para ele, e o Node.js sem tratador explícito se enquadra nesse caso. O resultado é que o Ctrl+C no terminal pode não encerrar o container, e o docker stop espera o prazo (10 segundos por padrão) e então envia SIGKILL. A opção --init do docker run (e init: true no Compose) coloca um pequeno processo init como PID 1, que repassa os sinais ao seu programa. Por isso os exemplos de API deste guia usam --init. Para encerrar um container em primeiro plano, use o Ctrl+C no terminal dele ou, em outro terminal, docker stop nome. O código de saída 137 que resulta de um encerramento forçado por SIGKILL não indica, por si só, falta de memória; a Etapa 11 mostra como distinguir os dois casos.
docker build executa o RUN uma vez e grava o resultado numa camada. O CMD fica guardado como metadado e só roda quando um container inicia, como PID 1.Diagrama: feito para este guiaOs conceitos-chave
Dockerfile
receita versionável que transforma decisões manuais em uma construção repetível.
FROM
toda imagem começa em outra; a escolha da base determina tamanho, compatibilidade e superfície de ataque.
WORKDIR, COPY, RUN
preparam o conteúdo da imagem durante o build.
CMD
o processo principal do container, executado no runtime.
EXPOSE
apenas documentação; quem publica a porta é --publish (ou ports no Compose).
Contexto de build
a pasta enviada ao Docker no docker build; só o que está nela pode ser copiado.
Tag de build
--tag nome:versão dá nome à imagem construída.
PID 1 e --init
o processo principal não recebe o tratamento padrão de sinais; o init repassa o SIGTERM e o SIGINT ao programa.
Exemplos comentados
Crie server.js:
// Importa apenas o módulo HTTP incluído no Node.js.const http = require("node:http");// Lê a porta do ambiente e mantém 3000 como padrão local.const port = Number(process.env.PORT || 3000);// Cria uma resposta JSON simples para qualquer requisição.const server = http.createServer((_request, response) => {response.writeHead(200, { "content-type": "application/json" });response.end(JSON.stringify({ status: "ok" }));});// Escuta em todas as interfaces do container.server.listen(port, "0.0.0.0", () => {console.log(`Servidor disponível na porta ${port}`);});
Crie Dockerfile:
Comandos essenciais desta etapa
CMD por outro comandoMão na massa
- Crie os dois arquivos do exemplo em um diretório vazio.
- Construa
minha-api:1.0e execute o container com--init, como no exemplo. - Em outro terminal, confirme a resposta com
curl http://localhost:3000e encerre o container comdocker stop minha-api(ou Ctrl+C no terminal dele). Confirme comdocker psque a porta 3000 ficou livre. - Troque o valor da resposta para
{"status":"pronto"}. - Reconstrua com a tag
minha-api:1.1. - Rode
docker images minha-apie observe que as versões são imagens distintas. - Rode
docker run --rm minha-api:1.1 node -e "console.log('outro comando')"e observe que oCMDfoi substituído sem reconstruir nada.
Dica: Guarde o Dockerfile no mesmo repositório do código. Assim, a receita da imagem é versionada junto com a aplicação, e um novo colega consegue reproduzir o ambiente com um único docker build.
Anota aí: Um Dockerfile transforma decisões manuais em uma construção repetível e versionável.
Etapa 04 otimização
Camadas, cache, imagens mínimas e multistage build
O problema
Um Dockerfile copia o projeto inteiro antes de instalar dependências. Qualquer mudança de uma linha invalida o cache e baixa tudo novamente. A imagem final também leva compiladores, caches e arquivos usados apenas no build. O resultado demora para construir, custa mais para transferir e oferece uma superfície de ataque maior.
O conceito
O Docker armazena o resultado de cada instrução como uma camada. Se encontra uma camada idêntica à que já construiu, ele a reaproveita (aparece como CACHED no build). Isso só acontece enquanto a instrução e seus arquivos de entrada não mudam, e, quando uma camada é invalidada, todas as seguintes também são. Por isso arquivos de dependências devem ser copiados antes do restante do código: copie primeiro o mínimo necessário para instalar as dependências, rode a instalação e só depois copie o resto do projeto.
A mesma lógica vale para qualquer gerenciador de pacotes (npm, pip, yarn, pnpm, composer); mudam só os nomes dos arquivos. Use também o modo que respeita o lockfile (npm ci, --frozen-lockfile) para builds previsíveis.
Um multistage build usa uma etapa rica em ferramentas para construir o artefato e uma etapa final menor para executá-lo. Você abre um FROM para o build, instala dependências e compila, e depois abre um novo FROM limpo, copiando apenas o resultado com COPY --from=. Ferramentas de build, node_modules de desenvolvimento e arquivos temporários não vão para a imagem final.
Variantes comuns de imagem base:
slim: base Debian reduzida, com boa compatibilidade. É um meio-termo entre peso e conveniência.alpine: distribuição mínima (poucos megabytes) que usamuslem vez daglibce o gerenciador de pacotesapk. Faltam coisas que costumam vir por padrão, comobashecurl; instale-as comapk addquando precisar. Pode exigir ajustes para dependências nativas.distroless: contém apenas o necessário para executar a aplicação, sem shell, sem gerenciador de pacotes e sem utilitários. É excelente para produção do ponto de vista de segurança, mas exige um build muito bem feito e normalmente depende de multistage.
Menor nem sempre significa melhor. Para desenvolvimento, ou enquanto você ainda não sabe o que vai precisar, comece com uma imagem mais completa e "seque" depois que tudo funcionar. Para produção, quando você já sabe exatamente o que roda, alpine ou slim reduzem o tamanho final, o tempo de transferência e a superfície de ataque. A base deve ser compatível, atualizada e suficiente para diagnosticar e executar a aplicação.
O exemplo multistage a seguir copia apenas /app/dist para o runtime. Isso só funciona quando dist é autossuficiente ou quando a aplicação usa apenas módulos nativos do Node. Se o artefato importa pacotes de node_modules, a etapa final também precisa receber as dependências de produção, por exemplo com uma instalação npm ci --omit=dev própria. Multistage não autoriza deixar dependências necessárias para trás.

Sem mudança nos arquivos, cada instrução reaproveita a camada já construída.

Os arquivos copiados mudaram: o COPY perde o cache, e todas as instruções depois dele também. Por isso o que muda menos vem primeiro.
Os conceitos-chave
Camada
resultado imutável de uma instrução; é a unidade de cache e de transferência.
Cache de build
reaproveita camadas que não mudaram; uma camada invalidada invalida as seguintes.
Ordem por frequência de mudança
o que muda pouco (dependências) vem antes do que muda muito (código).
Lockfile
package-lock.json e equivalentes; com npm ci a instalação segue exatamente o que está nele.
slim, alpine, distroless
três níveis de enxugamento, cada um com um custo de compatibilidade ou de diagnóstico.
Multistage build
separa construir de executar; COPY --from=etapa traz só o artefato.
Exemplos comentados
# ERRADO: copiar o projeto inteiro antes de instalar.# Qualquer alteração em qualquer arquivo invalida o COPY, e o npm ci roda de novo.FROM node:lts-alpineWORKDIR /appCOPY . .RUN npm ci
Comandos essenciais desta etapa
CACHEDbuild, para inspecioná-ladocker stop em outro terminalMão na massa
- Crie uma pasta com o
server.jsda Etapa 03, opackage.json, opackage-lock.json, obuild.jse o Dockerfile multistage desta etapa. - Confirme que o
package-lock.jsoné exatamente o fornecido acima. Ele permite quenpm cirode dentro do build sem exigir Node ou npm no host. - Rode
docker build --tag minha-app:otimizada .duas vezes. - Na segunda execução, observe
CACHEDnas etapas que não mudaram. - Altere apenas a mensagem de resposta em
server.jse reconstrua. - Confirme que a etapa
npm cicontinuou em cache e teste comdocker run --rm --init --publish 3000:3000 minha-app:otimizada, seguido decurl http://localhost:3000em outro terminal. Para encerrar, use Ctrl+C no terminal do container oudocker stopcom o ID mostrado pordocker ps, em outro terminal. Como o container foi criado sem--name, o Docker escolhe um nome aleatório; consulte-o emdocker ps. - Troque a ordem das instruções para o formato "ERRADO" (um
COPY . .antes donpm ci), altereserver.jsde novo e reconstrua. Observe que desta vez onpm ciroda novamente. Desfaça a troca. - Rode os três comandos de "sentindo a diferença entre bases" e anote o que está ausente no Alpine.
Dica: Se uma imagem pequena quebrar em produção por causa de uma biblioteca nativa, o problema costuma ser a diferença de musl (Alpine) para glibc (Debian). Antes de culpar o código, teste a mesma aplicação sobre slim. E lembre que uma imagem distroless não tem shell: para diagnosticar, será preciso usar outros meios (logs, uma imagem de depuração separada).
Anota aí: Ordene camadas pela frequência de mudança e deixe ferramentas de construção fora da imagem de runtime.
Etapa 05 volumes + mounts
Persistência com volumes, bind mounts e tmpfs
O problema
Um banco grava dados no sistema de arquivos do container. Durante uma atualização, o container é removido e recriado. A imagem continua disponível, mas os dados da camada gravável desaparecem junto com o container.
O conceito
A camada gravável do container é descartável por definição. Dados que precisam sobreviver à troca do container devem ficar em outro lugar, conectado a ele. O Docker oferece três mecanismos principais:
Volume nomeado. É a forma recomendada para produção. O Docker cria e registra o volume em seus metadados internos e o armazena, por padrão, no host em um caminho como /var/lib/docker/volumes/<nome>/_data; o que o container enxerga é esse diretório montado no caminho que você definiu (por exemplo, /var/lib/mysql). Pontos fortes: gerenciamento por comandos Docker, backup e migração mais simples, isolamento do restante do host. O caminho físico é um detalhe interno da Engine e pode variar conforme sistema e runtime, então prefira os comandos Docker em vez de manipular /var/lib/docker diretamente.
Bind mount. Aponta diretamente para um caminho específico do host, que o container enxerga exatamente como ele está, com todos os arquivos e permissões. É ótimo no desenvolvimento, porque você edita o código no host e o container vê a mudança em tempo real. Em contrapartida, não há isolamento (o container pode alterar o sistema de arquivos do host) e a portabilidade é limitada, porque depende da estrutura de pastas daquele computador.
tmpfs. Armazena os dados na memória do host. Ao parar ou reiniciar o container, tudo é perdido. É adequado para dados temporários ou sensíveis que, em geral, não devem ser gravados em disco (sob pressão de memória, o sistema pode mover páginas de tmpfs para a área de swap, então não o trate como garantia absoluta de que nada chega ao disco). tmpfs é um tipo de montagem, e não um driver de volume persistente.
Drivers de volume. O driver local é o padrão e usa o sistema de arquivos do host; atende bem à maioria dos casos e é o ponto de partida recomendado. Quando vários hosts precisam acessar o mesmo armazenamento, o volume pode usar NFS por meio do próprio driver local ou um plugin de terceiros compatível com o provedor (por exemplo, integrações com EBS, Azure Disk ou Ceph). Em ambientes com orquestrador na nuvem, drivers especializados ou plugins CSI no Kubernetes são as opções comuns. Essa escolha depende de rede, disponibilidade, permissões, backup e consistência exigida pela aplicação; só parta para ela se houver necessidade real de compartilhamento, persistência remota ou requisitos específicos de desempenho.
Os conceitos-chave
Camada gravável
efêmera; some com o container.
Volume nomeado
armazenamento administrado pelo Docker, independente do ciclo de vida do container.
Bind mount
um caminho do host montado dentro do container; conveniente, porém acoplado ao host.
tmpfs
montagem em memória, sem persistência.
Driver local
persiste no host em que o Docker está rodando.
Volume remoto (NFS ou plugin)
permite compartilhar armazenamento entre hosts, com complexidade extra.
:ro
sufixo que monta o caminho como somente leitura.
Exemplos comentados
# Cria um volume administrado pelo Docker.$ docker volume create dados-demo# Escreve um arquivo dentro do volume e remove o container ao terminar.$ docker run --rm \--volume dados-demo:/dados \alpine sh -c 'echo persistiu > /dados/mensagem.txt'# Um novo container lê o mesmo volume.$ docker run --rm \--volume dados-demo:/dados:ro \alpine cat /dados/mensagem.txt# Exibe metadados do volume.$ docker volume inspect dados-demo# saída esperadapersistiu
O sufixo :ro monta o volume como somente leitura no segundo container.
Comandos essenciais desta etapa
local por padrão)-v nome:/caminho): monta um volume nomeadoMão na massa
- Execute os comandos do primeiro exemplo (volume
dados-demo). - Confirme que o primeiro container não existe mais com
docker ps --all. - Leia o arquivo usando um segundo container.
- Prove que o tmpfs não sobrevive ao reinício: rode o exemplo do tmpfs (
docker run --detach --init --name tmp-demo --tmpfs /dados alpine sleep 300, grave comdocker exec tmp-demo sh -c 'echo x > /dados/a', rodedocker restart tmp-demoe liste comdocker exec tmp-demo ls -A /dados). A listagem deve sair vazia. Remova o container comdocker rm --force tmp-demo. - Rode o exemplo do bind mount e confirme que a edição feita no host aparece na resposta sem reiniciar o container.
- Confirme que o volume contém apenas dados de laboratório. Então execute
docker volume rm dados-demo. A remoção é permanente e não oferece lixeira ou recuperação automática.
Dica: Para qualquer dado que não possa ser perdido (banco de dados, uploads), use volume nomeado. Reserve o bind mount para o código que você edita durante o desenvolvimento, e lembre que um bind mount em produção amarra a aplicação à estrutura de pastas daquele servidor.
Anota aí: Se um dado precisa sobreviver ao container, ele não pode depender da camada gravável desse container.
Etapa 06 conectividade
Redes, DNS interno e publicação de portas
O problema
Uma API precisa acessar um banco. Usar o IP atual do container funciona até ele ser recriado e receber outro endereço. Publicar a porta do banco no host resolve o acesso, mas expõe um serviço que deveria ser interno.
O conceito
Cada container recebe seu próprio namespace de rede e um IP próprio dentro de uma rede Docker. Quando você cria uma rede personalizada com docker network create, o Docker habilita nela um DNS interno: cada container conectado pode ser encontrado pelo nome definido em --name. A API se conecta ao banco usando o hostname banco, sem IP fixo; uma aplicação Node.js falaria com um MongoDB apenas apontando a URI para mongodb://mongo:27017, desde que o container se chame mongo e ambos estejam na mesma rede.
As redes personalizadas também são isoladas por padrão: só containers conectados à mesma rede se enxergam. Isso permite isolar bancos de dados, separar ambientes (dev e prod) e reduzir a superfície de ataque. Containers podem ser conectados e desconectados de redes a qualquer momento, o que dá controle fino sobre quem conversa com quem. A mesma lógica de redes é usada pelo Docker Compose (Etapa 07) e por orquestradores como o Kubernetes.
Os modos de rede são:
bridge: rede isolada no mesmo host, com IP por container. Redes bridge criadas pelo usuário oferecem resolução automática por nome. É o modo do dia a dia, indicado para desenvolvimento local e ambientes simples em um só host.host: o container compartilha a pilha de rede do host, sem NAT e sem IP próprio. Dá mais desempenho e acesso direto à rede do host (útil para monitoramento e proxies locais), mas elimina o isolamento de rede e pode causar conflito de portas.none: desabilita a conectividade de rede do container. Útil para testes e cargas que não devem se comunicar com nada.overlay: conecta containers em hosts diferentes, usado com o Docker Swarm. Outras redes avançadas (comomacvlan) existem para cenários específicos de orquestração e ficam fora deste guia.
A rede bridge padrão (a que o container recebe quando você não especifica nada) e uma bridge criada pelo usuário não são equivalentes. O DNS por nome de container é uma vantagem da rede personalizada. Na bridge padrão, não dependa dessa descoberta automática.
Descoberta por nome é diferente de acesso externo. EXPOSE 3000 documenta uma porta na imagem. --publish 8080:3000 cria o encaminhamento da porta 8080 do host para a porta 3000 do container. Para serviços que só outros containers acessam (como o banco), não publique nada.
DNS não transmite dados nem autentica participantes: ele só resolve nomes para endereços. Isolamento, criptografia e autorização exigem mecanismos próprios.
--publish recebe tráfego de fora. Na bridge padrão não há resolução por nome.Diagrama: feito para este guiaOs conceitos-chave
Rede bridge personalizada
isola um grupo de containers e oferece DNS por nome.
DNS interno
traduz o nome do container para o IP atual dele.
host
sem isolamento de rede; só para casos específicos.
none
sem rede alguma.
overlay
rede entre hosts, pertencente ao universo do Swarm.
Publicação de porta (--publish)
abre um acesso do host para um container; sem ela, o serviço só é visível dentro da rede.
EXPOSE
documentação na imagem, sem efeito de publicação.
docker network connect e disconnect
ligam um container que já está em execução a uma rede, ou o desligam dela.
Exemplos comentados
# Cria uma bridge com DNS interno.$ docker network create rede-demo# Inicia um servidor chamado web nessa rede.$ docker run --detach --name web --network rede-demo nginx:alpine# Um container temporário resolve o hostname web e faz uma requisição.$ docker run --rm --network rede-demo curlimages/curl \--silent --head http://web# Mostra quais containers estão conectados.$ docker network inspect rede-demo# Limpa os recursos do exercício.$ docker rm --force web$ docker network rm rede-demo# saída esperadaHTTP/1.1 200 OKServer: nginx/...
Nenhuma porta foi publicada. A comunicação aconteceu internamente, pelo hostname web.
Comandos essenciais desta etapa
--driver bridge)-p): publica uma portadocker run --network none imagem: modos host e noneMão na massa
- Rode o primeiro exemplo (
rede-demo) sem os dois comandos de limpeza do final (docker rm --force webedocker network rm rede-demo), porque os passos seguintes ainda precisam do containerwebe da rede. Confirme o statusHTTP/1.1 200 OK. - Execute
docker run --rm curlimages/curl --head http://web, sem--network rede-demo. - Observe a falha de resolução, pois o novo container está fora da rede. A saída esperada é
curl: (6) Could not resolve host: web. - Crie um cliente com
docker run --detach --name cliente alpine sleep 300. - Conecte-o com
docker network connect rede-demo cliente. - Prove a descoberta por nome com
docker exec cliente wget -qO- http://web. A saída deve conter o HTML de boas-vindas do Nginx (<title>Welcome to nginx!</title>). - Inspecione as conexões com
docker network inspect rede-demoe localizewebeclientena lista de containers. - Desconecte o cliente com
docker network disconnect rede-demo cliente, repita owgete observe que a resolução falha de novo, comwget: bad address 'web'. - Confira os nomes e remova os containers de laboratório com
docker rm --force cliente web; depois remova a rede comdocker network rm rede-demo.--forceencerra os containers sem a parada normal e só é apropriado aqui porque ambos são descartáveis. - Rode o exemplo "a bridge padrão não resolve nomes" e compare o erro (
wget: bad address 'alvo') com o sucesso do passo 6.
Dica: Para expor um serviço só ao seu computador (por exemplo, enquanto desenvolve), publique no endereço de loopback: --publish 127.0.0.1:8080:80. Assim a porta não fica acessível para outras máquinas da rede.
Anota aí: Containers na mesma rede personalizada se encontram pelo nome; publique somente as portas que precisam ser acessadas pelo host.
Etapa 07 multicontainer
Docker Compose para aplicações multicontainer
O problema
Subir API, banco, rede e volume com vários comandos funciona até alguém esquecer uma flag, usar um nome diferente ou executar os passos na ordem errada. O ambiente deixa de ser uma configuração versionável e vira uma sequência oral de instruções.
O conceito
O Docker Compose descreve uma aplicação inteira em um único arquivo YAML (por padrão compose.yaml; o nome mais antigo docker-compose.yml continua aceito). Com um comando você sobe, observa e derruba todos os containers, redes e volumes. O Compose cria automaticamente uma rede para o projeto, e os serviços se encontram pelo nome do serviço (db, web), com a mesma lógica de DNS da Etapa 06. O nome do projeto é, por padrão, o nome da pasta, e os containers recebem nomes como projeto-web-1.
Os elementos principais do arquivo são:
services: os containers que formam a aplicação.buildouimage: como obter a imagem de cada serviço.ports: portas publicadas no host.volumes: persistência ou compartilhamento de arquivos.networks: limites de comunicação.environmenteenv_file: configuração não secreta.secrets: arquivos montados para dados sensíveis (Etapa 10).init:init: trueexecuta um init como PID 1 do serviço, equivalente ao--initdodocker run(Etapa 03).
Em vez de image:, um serviço pode usar um bloco build: para ser construído localmente antes de subir. Três campos dão controle sobre isso: context (a pasta usada como base do build), dockerfile (qual arquivo usar, útil para Dockerfile.dev e Dockerfile.prod) e args (parâmetros de build declarados com ARG no Dockerfile). Isso evita acumular Dockerfiles quase idênticos e permite que qualquer pessoa clone o repositório e rode docker compose up com builds adaptados ao fluxo do time.
O Compose não resolve tudo sozinho. Ele interpreta a definição e coordena recursos Docker, mas a aplicação ainda precisa lidar corretamente com inicialização, falhas transitórias e dependências. depends_on garante a ordem de criação, e não que o banco esteja pronto; a prontidão é tratada com healthchecks nas Etapas 08 e 12.

Os conceitos-chave
Arquivo Compose
descrição versionável de serviços, redes e volumes.
Serviço
um container (ou um grupo de réplicas) definido no arquivo.
Rede do projeto
criada automaticamente; o nome do serviço funciona como hostname.
build (context, dockerfile, args)
constrói a imagem do serviço localmente.
depends_on
ordem de criação, e não prontidão.
init: true
coloca um init como PID 1, para que o docker compose down encerre o serviço Node sem esperar o prazo.
Volume declarado
o bloco volumes: do arquivo cria o volume nomeado usado pelos serviços.
docker compose config
mostra a configuração consolidada e acusa erros de YAML.
Exemplos comentados
services:web:# Constrói usando o Dockerfile do diretório atual.build:context: .dockerfile: Dockerfile# Executa um init como PID 1, para que o Node receba o SIGTERM e o down seja rápido.init: true# Publica apenas o serviço que recebe tráfego externo.ports:- "3000:3000"# O hostname db será resolvido pelo DNS da rede do Compose.environment:DB_HOST: db# Expressa a dependência de criação, não a prontidão do banco.depends_on:- dbdb:# Fixa a versão principal da imagem oficial.image: postgres:16-alpineenvironment:POSTGRES_PASSWORD: exemplo-local# Persiste os dados fora do ciclo de vida do container.volumes:- dados_db:/var/lib/postgresql/data# Declara o volume nomeado usado pelo banco.volumes:dados_db:
Comandos:
Comandos essenciais desta etapa
docker compose logs db)docker compose start serviço: param e retomam um serviçoMão na massa
- Salve o exemplo como
compose.yamlao lado doDockerfilee doserver.jsda Etapa 03. - Rode
docker compose configantes de iniciar. Esse comando mostra a configuração consolidada e detecta erros de YAML. - Execute
docker compose up --detach --build. - Consulte
docker compose psedocker compose logs db. - De dentro do serviço
web, confirme a descoberta por nome do banco comdocker compose exec web nslookup db. A saída deve mostrar o nomedbe um endereço IP interno. - Finalize com
docker compose down. Não use--volumesse quiser preservar os dados. Depois rodedocker volume lse observe que o volume do banco continua lá, com o nome<pasta>_dados_db.
Dica: Rode docker compose config sempre que editar o YAML, antes de up. Ele pega erros de indentação e mostra os valores finais (variáveis expandidas, arquivos combinados), o que poupa muita tentativa e erro.
Anota aí: Compose transforma uma sequência frágil de comandos em uma definição versionável da aplicação.
Etapa 08 operações
Ambientes, reinício, escala e CI
O problema
Desenvolvimento precisa de bind mount e logs detalhados. Produção precisa de imagem pronta e menos exposição. Copiar todo o Compose para cada ambiente cria arquivos quase iguais que divergem com o tempo. Além disso, um processo pode falhar temporariamente e não voltar sozinho, e uma pipeline de testes precisa de um ambiente descartável parecido com o real.
O conceito
Vários arquivos Compose. O Compose combina arquivos na ordem fornecida por -f, e os posteriores sobrepõem os anteriores, desde que os serviços mantenham o mesmo nome. Um arquivo de produção pode substituir somente imagem, portas ou variáveis necessárias, sem duplicar o YAML base:
docker compose \-f compose.yaml \-f compose.prod.yaml \config
Use config para conferir o resultado antes de subir. Por padrão, docker compose up (sem -f) carrega automaticamente compose.yaml e, se existir, compose.override.yaml (nos nomes antigos, docker-compose.yml e docker-compose.override.yml). Isso torna o override o lugar natural dos ajustes de desenvolvimento. Ao usar -f explicitamente, somente os arquivos listados são carregados.
Políticas de reinício:
no(padrão): não reinicia automaticamente.on-failure: reinicia quando o processo termina com código diferente de zero; aceita um limite de tentativas (on-failure:3).always: tenta manter o container ativo, qualquer que seja o motivo da parada.unless-stopped: semelhante aalways, mas respeita uma parada manual.
Não coloque always em tudo: entenda o papel de cada serviço. Um banco costuma ficar bem com unless-stopped; uma API que depende do banco pode usar on-failure com limite, para evitar laços infinitos quando o erro é de configuração; um consumidor de fila que deve estar sempre ativo pode usar always. Política de reinício não resolve ordem de inicialização nem garante que uma dependência esteja pronta. Para prontidão, combine healthcheck e dependências condicionadas (depends_on com condition: service_healthy) quando sua versão do Compose oferecer suporte, e mantenha retentativas na própria aplicação.
Escala. docker compose up --scale worker=3 cria réplicas de um serviço. Cada réplica é uma instância independente, e um mapeamento fixo de porta como 8080:3000 impede várias réplicas no mesmo host, porque a porta 8080 só pode ser ocupada uma vez. Réplicas atrás de uma porta única exigem um balanceador na frente delas.
CI (integração contínua). A mesma definição que roda na máquina de quem desenvolve pode ser usada numa pipeline: subir as dependências (por exemplo, aplicação Node.js e um banco Postgres), rodar os testes de integração e remover tudo. Isso traz consistência entre o ambiente local e a esteira de build, evita depender de serviços externos ou de mocks para tudo e aproxima o ambiente de testes do de produção. O Compose funciona em praticamente qualquer provedor de CI (GitHub Actions, GitLab CI, Bitbucket, Jenkins).
-f, o arquivo seguinte sobrepõe o anterior no serviço de mesmo nome: só a imagem muda, e o resto vem do arquivo base. Confira sempre com config.Diagrama: feito para este guiaOs conceitos-chave
Override
arquivo que altera só o que varia entre ambientes.
-f múltiplo
define explicitamente quais arquivos combinar e em qual ordem.
restart
reação a encerramentos do processo; não substitui verificação de prontidão.
on-failure:N
reinicia no máximo N vezes após falhas.
--scale
cria réplicas; porta fixa no host limita a uma réplica.
--wait
faz o up aguardar até os serviços com healthcheck ficarem saudáveis.
CI com Compose
ambiente descartável, subir, testar e derrubar.
Exemplos comentados
compose.yaml:
services:web:# Usa uma imagem pública para que o exemplo rode sem projeto externo.image: nginx:alpine# Reinicia somente diante de falha do processo, até três vezes.restart: on-failure:3labels:ambiente: desenvolvimentohealthcheck:# wget já existe nesta variante da imagem.test: ["CMD", "wget", "--quiet", "--spider", "http://localhost"]interval: 5stimeout: 2sretries: 5
compose.prod.yaml:
Comandos essenciais desta etapa
Mão na massa
- Crie uma pasta vazia e salve nela os dois arquivos YAML do exemplo "base e produção". Nenhum Dockerfile ou código adicional é necessário.
- Compare
docker compose -f compose.yaml configcomdocker compose -f compose.yaml -f compose.prod.yaml config. - Observe a tag da imagem e o valor da label
ambienteno resultado consolidado: no primeiro comando aparecemnginx:alpineedesenvolvimento; no segundo,nginx:stable-alpineeproducao. - Execute
docker compose -f compose.yaml -f compose.prod.yaml up --detach --wait. - Confirme
healthycomdocker compose -f compose.yaml -f compose.prod.yaml ps. A coluna de status deve terminar em(healthy). - Valide o Nginx com
docker compose -f compose.yaml -f compose.prod.yaml exec web nginx -t. A saída deve terminar emtest is successful. Encerre comdocker compose -f compose.yaml -f compose.prod.yaml down. - Em outra pasta, rode o exemplo de reinício e escala. Confirme três réplicas do
workeremdocker compose pse oreinicios=3dofalhador. Encerre comdocker compose down. - Ainda nessa pasta, acrescente
ports: ["8080:80"]ao serviçoworkere rodedocker compose up --detach --scale worker=2. Observe o conflito de porta: uma das réplicas não sobe e o Docker informaBind for 0.0.0.0:8080 failed: port is already allocated. Finalize comdocker compose downpara liberar a pasta e a porta.
Dica: Comece sempre por docker compose config ao combinar arquivos. Ver o resultado consolidado antes de subir evita a surpresa de um override que não sobrescreveu o que você esperava (ou de um -f que desligou o override automático).
Anota aí: Arquivos Compose adicionais mudam apenas o que varia entre ambientes; reinício recupera processos, mas não substitui verificação de prontidão.
Etapa 09 menor privilégio
Segurança em camadas
O problema
Uma aplicação vulnerável roda como root, recebe mais capabilities do que precisa e carrega um token dentro da imagem. Se houver exploração, o invasor encontra permissões e dados que nunca foram necessários para a tarefa do serviço.
O conceito
Como o container compartilha o kernel com o host, não basta confiar no isolamento padrão: um escape do container pode dar acesso ao sistema. A defesa vem da combinação de várias camadas, cada uma reduzindo o que uma exploração conseguiria fazer:
- Namespaces isolam a visão de recursos (Etapa 01).
- Capabilities dividem os privilégios tradicionais do root em permissões menores. Por padrão, o root do container já vem com menos poderes que o root do host. Você remove capacidades perigosas (como
NET_ADMINeSYS_ADMIN) com--cap-drope concede uma específica com--cap-addsomente se o serviço provar que precisa dela. A regra é o menor privilégio. - Seccomp funciona como um filtro de chamadas de sistema (syscalls). O Linux tem centenas delas, e o seccomp permite bloquear várias, com base em uma lista permitida. O Docker já traz um perfil padrão que bloqueia chamadas perigosas, e é possível fornecer um perfil próprio, mais restritivo, com
--security-opt seccomp=perfil.json. - AppArmor ou SELinux, quando habilitados no host, acrescentam controle de acesso obrigatório.
- Usuário não root reduz o impacto de uma exploração. Por padrão, o container roda como root; crie um usuário sem privilégios na imagem e use
USER. - Permissões mínimas evitam
chmod 777como solução genérica: entenda quem deve ser dono de quê e conceda o mínimo necessário. - Sistema de arquivos somente leitura (
--read-only), comtmpfsapenas onde a aplicação precisa gravar, impede que um invasor altere a imagem em execução. - Imagem pequena e atualizada reduz ferramentas e vulnerabilidades disponíveis.
- Segredos em arquivos próprios evitam valores sensíveis no Dockerfile e no ambiente (Etapa 10).
O usuário root dentro de um container não é automaticamente idêntico ao root do host, pois namespaces e capabilities impõem limites. Ainda assim, executar sem necessidade como UID 0 aumenta o risco, especialmente com mounts perigosos, capabilities extras ou falhas do runtime.
Ao criar o usuário na imagem, use adduser (ou useradd, conforme a distribuição base) e defina o dono dos arquivos copiados com --chown. Ao usar volumes ou bind mounts, o usuário do host pode ser diferente do usuário do container, o que causa problemas de permissão; a solução comum é executar o container com os mesmos UID e GID do host (--user "$(id -u):$(id -g)").
Namespaces não são paredes invioláveis e USER node não resolve segurança sozinho. A defesa vem da combinação de controles, e a implementação real pertence ao kernel, ao runtime, à imagem e às opções de execução. Não monte /var/run/docker.sock em aplicações comuns: quem controla esse socket pode controlar a Docker Engine e, na prática, comprometer o host.
docker.sock anula todas, porque dá controle do daemon, que roda como root no host.Diagrama: feito para este guiaOs conceitos-chave
Defesa em profundidade
várias camadas, nenhuma suficiente sozinha.
Usuário não root (USER)
o processo roda sem privilégios administrativos.
--chown
define o dono dos arquivos copiados para a imagem.
--cap-drop ALL e --cap-add
remove todas as capabilities e devolve só as indispensáveis.
Seccomp
filtro de chamadas de sistema; há um perfil padrão do Docker.
--read-only e --tmpfs
raiz imutável com áreas gravíveis e efêmeras apenas onde necessário.
--user uid:gid
faz o container usar os mesmos IDs do host, evitando problemas de permissão em mounts.
docker.sock
acesso a ele equivale a controle sobre o Docker; não monte em aplicações comuns.
Exemplos comentados
# Modelo para uma aplicação Node.js em Alpine.FROM node:lts-alpine# Cria um usuário sem privilégios; -D dispensa senha (comando do Alpine).# Em bases Debian o equivalente é: RUN useradd -m appuserRUN adduser -D appuserWORKDIR /app# Copia os arquivos já pertencendo ao usuário da aplicação.COPY --chown=appuser:appuser . .# Instala dependências conforme o lockfile, ainda com privilégio de build.RUN npm ci --omit=dev# As instruções seguintes e o processo principal rodam sem privilégios.USER appuserCMD ["npm", "start"]
Esse modelo garante que existe um usuário sem privilégios de root, que os arquivos foram copiados com a permissão correta e que toda a aplicação roda com permissões limitadas. Em uma imagem oficial do Node, o usuário node já existe, e basta usar COPY --chown=node:node e USER node. Uma versão mínima e executável para ver o efeito:
Comandos essenciais desta etapa
/tmp gravável em memóriaMão na massa
- Rode
docker run --rm node:lts-alpine ide observe o usuário padrão da imagem (uid=0(root)). - Rode
docker run --rm --user node node:lts-alpine ide confirmeuid=1000(node). - Prove o
--read-onlycomo root: rodedocker run --rm alpine sh -c 'touch /liberado && echo "escrita permitida"'e depoisdocker run --rm --read-only alpine touch /bloqueado. A saída do segundo deve sertouch: /bloqueado: Read-only file system. - Execute o comando completo do exemplo de runtime com
--read-onlye--tmpfs /tmp, e confirme as duas mensagens esperadas:raiz bloqueadaetmpfs gravável. - Remova
--tmpfs /tmpdo comando combinado e repita. A escrita em/tmptambém falha, pois ele volta a fazer parte da raiz somente para leitura. A saída esperada éraiz bloqueada, seguida desh: can't create /tmp/teste: Read-only file systeme decat: can't open '/tmp/teste': No such file or directory. - Rode os dois comandos do exemplo de capabilities e compare o resultado.
- Construa e execute o
usuario-demoe confirme que oidmostraappuser, e nãoroot. - Rode o exemplo de UID e GID e confirme que os donos dos dois arquivos são diferentes e que a edição do arquivo do root pelo host é negada (
Permission denied).
Dica: Quando uma aplicação falhar depois de --cap-drop ALL, não volte atrás por reflexo. Leia o erro, identifique qual operação foi negada e devolva apenas a capability correspondente com --cap-add. É assim que o conjunto mínimo se descobre.
Anota aí: Segurança de container é defesa em profundidade: usuário, permissões, capabilities, chamadas de sistema, imagem e mounts precisam cooperar.
Etapa 10 supply chain
Contexto de build, segredos e análise de imagem
O problema
Um docker build . envia .git, node_modules, logs e .env para o contexto. O build demora, o cache invalida sem necessidade e um segredo pode acabar copiado para uma camada. Mesmo apagado numa instrução posterior, ele pode permanecer no histórico de uma camada anterior. Em outro ponto do ciclo, uma senha colocada em environment ou em um .env aparece em comandos de inspeção e pode ir parar no Git.
O conceito
O contexto de build é tudo o que está na pasta indicada no docker build (ou no caminho que você passar). Por padrão, o Docker tenta enviar tudo, incluindo .git, node_modules, arquivos grandes e segredos. O `.dockerignore` funciona como um .gitignore para o build: lista o que não deve ser enviado ao contexto. O resultado é um build mais rápido, uma imagem menor e mais segura, e menos invalidações de cache. Mantenha o .dockerignore na raiz do projeto, perto do Dockerfile, e rode o docker build o mais perto possível do Dockerfile. Se o build parece pesado demais, confira o que está no contexto: é comum enviar gigabytes sem perceber.
Considere o projeto:
meu-app/├── node_modules/├── .env├── .git/├── build/├── src/├── Dockerfile└── compose.yaml
Um .dockerignore adequado:
# Dependências locais serão instaladas dentro da imagem.node_modules# Configuração local e possíveis segredos não entram no contexto..env.env.*# Histórico, metadados e arquivos temporários são desnecessários..git*.logcoveragedistbuild
Isso evita enviar milhares de arquivos de node_modules, impede que o .env vá parar dentro da imagem, evita copiar o histórico da pasta .git e impede que artefatos de builds anteriores entrem por engano. Cada stack tem necessidades diferentes, então revise qualquer .dockerignore sugerido por ferramentas ou assistentes de código para a sua realidade.
Variáveis de ambiente e segredos. env_file é prático para configuração local, mas não transforma valores em segredos. Eles podem aparecer na configuração e na inspeção do container (docker inspect). Arquivos .env com credenciais não devem ser versionados. A diretiva secrets: do Compose monta o valor como arquivo dentro do container, em /run/secrets/<nome>, o que evita deixá-lo no ambiente do processo. Em desenvolvimento local, env_file continua prático; quando a aplicação passa a ter staging ou produção, migre os dados críticos para secrets.
Parâmetros de build. ARG declara um valor disponível durante a construção, e build.args fornece esse valor pelo Compose. Eles servem para dados não confidenciais, como versão, canal de distribuição ou seleção de recurso. Não use ARG para senha ou token: valores de build podem aparecer no histórico, em metadados e em registros da pipeline.
Ferramentas de análise de imagem. Passada a fase de escrever um Dockerfile enxuto, vale inspecionar a imagem de verdade:
- Dive mostra o conteúdo e o peso de cada camada: o que cada
RUN,COPYeADDadicionou, o que está pesando e o que pode sair. - Snyk verifica vulnerabilidades conhecidas na imagem e em dependências, usando uma base de dados de vulnerabilidades mantida continuamente. Funciona por linha de comando e se integra a pipelines de CI.
Um complementa o outro: o Dive ajuda a entender o tamanho, e o Snyk cuida da segurança.
.dockerignore decide o que nem chega ao builder: o build fica mais rápido e o .env não entra em nenhuma camada. Um segredo copiado no build continua no histórico, mesmo apagado numa instrução seguinte.Diagrama: feito para este guiaOs conceitos-chave
Contexto de build
o que é enviado ao Docker no build; tudo nele pode afetar tempo, cache e exposição.
.dockerignore
filtra o contexto antes do envio.
Segredo em camada
um segredo copiado em uma camada permanece no histórico, mesmo que depois seja apagado.
env_file
conveniente, mas não protege valores.
secrets (Compose)
monta o valor como arquivo em /run/secrets/<nome>.
ARG / build.args
parâmetros de build não confidenciais.
Dive e Snyk
análise de camadas e de vulnerabilidades.
Exemplos comentados
Use uma pasta com este Dockerfile e o compose.yaml logo abaixo.
# Usa uma base concreta antes de declarar o argumento do estágio.FROM node:lts-alpine# Declara um parâmetro não confidencial com valor padrão.ARG APP_VERSION=dev# Registra a versão como metadado consultável na imagem.LABEL org.opencontainers.image.version=$APP_VERSION
Comandos essenciais desta etapa
env_file; útil para perceber o que fica expostoMão na massa
- Entre na pasta da Etapa 04 e crie o peso de propósito:
mkdir -p node_modules && head -c 50000000 /dev/urandom > node_modules/pesado.bin. - Rode
docker build --progress plain --tag minha-app:analise .uma única vez e anote o tamanho do contexto (cerca de 50 MB). - Crie o
.dockerignoredo exemplo na mesma pasta, repita o build e compare o tamanho enviado (alguns kB) e o reaproveitamento do cache. - Use
docker history minha-app:analisepara identificar instruções que adicionaram mais dados. Remova o peso artificial comrm -rf node_modules. - Reproduza o exemplo "um segredo que sobrevive no histórico": confirme no
docker historya linha doCOPYcom 21B (ou um valor um pouco maior e arredondado, se o Docker usar o image store do containerd) e a doRUN rmcom 0B, e rode o roteiro comdocker savepara versenha-de-laboratorioaparecer em texto puro. - Reproduza o exemplo do
ARGcomimage: versao-demoe confirme que odocker inspectimprime1.4.0. - Se Dive ou Snyk estiver instalado, analise a mesma tag e registre um ponto de melhoria.
Dica: O BuildKit também permite entregar segredos ao build sem gravá-los em nenhuma camada. Se um passo do build realmente precisa de uma credencial (por exemplo, baixar um pacote privado), consulte a documentação do Docker sobre segredos de build em vez de recorrer a ARG ou COPY.
Anota aí: Um segredo que não entra no contexto não pode vazar por uma camada criada durante o build.
Etapa 11 diagnóstico
Troubleshooting com uma ordem de investigação
O problema
Um container entra em Exited (1). Reiniciá-lo repetidamente não explica a causa. Alterar várias configurações ao mesmo tempo pode esconder o sintoma e criar outros problemas.
O conceito
Use uma sequência previsível, da evidência mais barata à mais específica:
- Estado:
docker ps --allmostra se o container está rodando, encerrado ou reiniciando. - Logs:
docker logsmostra stdout e stderr do processo. - Configuração:
docker inspectrevela comando, mounts, rede, portas e estado. - Eventos:
docker eventsmostra criação, início, parada, kill e remoção em tempo real. - Processo e recursos:
docker topedocker statsmostram processos e consumo. - Teste interno:
docker execexecuta uma verificação dentro de um container ainda ativo.
O docker inspect devolve JSON detalhado e é a fonte de respostas para perguntas como: qual comando original está sendo executado (Config.Cmd), quais volumes estão montados e onde (Mounts), quais portas estão expostas e mapeadas (NetworkSettings.Ports), qual é a imagem (Image) e se o container está parado por erro ou reiniciando (State). Para saber se o container morreu por falta de memória, verifique State.OOMKilled. A opção --format extrai exatamente o campo desejado, e a saída completa pode ser combinada com ferramentas como jq ou usada em scripts de shell.
O código de saída conta uma parte da história: 0 indica término normal; qualquer outro valor vem do próprio programa ou do Docker. Valores conhecidos incluem 125 (o Docker falhou ao executar o docker run), 126 (o comando existe mas não pôde ser invocado), 127 (o comando não foi encontrado) e 137 (o processo recebeu SIGKILL). O 137 tem mais de uma origem: pode ser falta de memória (nesse caso OOMKilled aparece como true), mas também aparece quando um docker stop esgota o prazo sem que o processo atenda ao SIGTERM e o Docker envia SIGKILL (nesse caso OOMKilled continua false; é o efeito do PID 1 sem tratador de sinal descrito na Etapa 03). Confirme sempre com OOMKilled antes de concluir que faltou memória.
O docker events é leve e nativo, e acompanha mudanças de estado de containers, imagens, volumes e redes, com timestamps. É especialmente útil no troubleshooting em tempo real, ao testar scripts de automação ou durante deploys. Pode ser filtrado por tipo (--filter type=container) ou por container (--filter container=nome) e a saída pode alimentar scripts ou um sistema de logs.
docker restart sem evidência desliga e liga o sistema sem ler o que ele disse: pode devolver o serviço por alguns minutos, mas também apaga o contexto operacional e não identifica a causa. A ordem acima protege o diagnóstico contra palpites: a inspeção vem antes de qualquer reinício.
Os conceitos-chave
Estado e código de saída
o primeiro dado a olhar, antes de logs.
Logs
a mensagem que o próprio processo deixou.
docker inspect --format
extrai um campo específico dos metadados.
OOMKilled
indica que o container foi morto por falta de memória.
Código 137
SIGKILL; pode ser falta de memória (OOMKilled=true) ou um docker stop que esgotou o prazo (OOMKilled=false).
docker events
linha do tempo de tudo o que o Docker fez.
docker top e docker stats
processos e consumo em tempo real.
Hipótese antes da evidência
formule o que espera encontrar e confirme.
Exemplos comentados
# O shell registra uma mensagem de erro e termina com código 42.$ docker run --name falha alpine \sh -c 'echo "configuração ausente" >&2; exit 42'# Confirma o código de saída sem ler todo o JSON.$ docker inspect --format \'status={{.State.Status}} exit={{.State.ExitCode}} oom={{.State.OOMKilled}}' \falha# Recupera a mensagem emitida pelo processo.$ docker logs falha# saída esperadaconfiguração ausentestatus=exited exit=42 oom=falseconfiguração ausente
A mensagem configuração ausente aparece duas vezes: a primeira é o stderr do próprio docker run, mostrado no terminal enquanto o container executava; a segunda é a mesma mensagem recuperada depois pelo docker logs, que lê o que ficou registrado. O container continua existindo (parado) até você removê-lo, e a remoção fica para o fim do diagnóstico:
Comandos essenciais desta etapa
docker logs --tail 50 nome mostra só o finalrunning, exited, ...)docker events --filter type=container e docker events --filter container=nome restringem a saída--no-stream faz uma leitura única)Mão na massa
- Provoque a falha rodando só o primeiro comando do exemplo (o
docker run --name falha ...), sem odocker rm: a remoção fica para o passo 5. A mensagemconfiguração ausentejá aparece no terminal durante esse comando. - Antes de olhar os logs, formule uma hipótese apenas com estado e exit code: rode o
docker inspect --formatdo exemplo e leiastatus=exited exit=42 oom=false. - Confira a hipótese com
docker logs falha. A mesma mensagem aparece uma segunda vez, agora vinda do registro do Docker. - Use
docker inspect --format '{{json .Config.Cmd}}' falhapara ver o comando original. - Remova o container somente depois de registrar a causa:
docker rm falha. - Rode
docker run --rm alpine comando-inexistente, leia o erro e confira o código comecho $?. A saída esperada é127. - Com
docker events --filter type=containeraberto em um terminal, rodedocker run --rm --name evento alpine trueem outro e identifique cada evento (create,attach,start,die,destroy).
Dica: Escreva sua hipótese antes de olhar cada evidência, mesmo que em uma linha. Isso treina a ler o estado e o código de saída com atenção, em vez de sair rodando comandos até algo parecer certo.
Anota aí: Diagnóstico começa pelo estado observável e avança para evidências; reiniciar não é investigar.
Etapa 12 observabilidade
Logs, healthcheck e prontidão operacional
O problema
O processo está ativo, então o container aparece como Up. Mesmo assim, a aplicação não consegue atender requisições porque perdeu acesso a uma dependência. Em outro incidente, o container foi recriado e seus logs locais desapareceram.
O conceito
Um healthcheck executa periodicamente uma verificação e classifica o container como starting, healthy ou unhealthy. O estado do processo indica presença; o healthcheck verifica capacidade de serviço. Ele não corrige a aplicação sozinho: a política do runtime ou do orquestrador decide como reagir. Um bom healthcheck também funciona como alerta para instabilidade.
Um resultado healthy tem alcance limitado. Ele prova apenas a condição examinada pelo teste naquele momento. Um endpoint raso pode passar no teste enquanto uma função importante está quebrada; um teste pesado demais pode criar carga e falso alarme. O healthcheck precisa representar a função certa com custo controlado.
Os parâmetros principais são test (o comando), interval (de quanto em quanto tempo), timeout (quanto esperar por uma tentativa), retries (quantas falhas seguidas marcam unhealthy) e start_period (um período inicial em que falhas não contam, para a aplicação ter tempo de subir). Ele pode ser declarado no Dockerfile (instrução HEALTHCHECK), no Compose (healthcheck:) ou no docker run (--health-cmd e opções relacionadas).
Logs. Aplicações devem escrever logs em stdout e stderr, que é o que docker logs lê. Mas confiar apenas em docker logs deixa de bastar com múltiplas instâncias: os logs ficam fragmentados entre containers e, se o container for destruído, geralmente se perdem. A solução é centralizar e estruturar. Logs estruturados são registros em formato padronizado (geralmente JSON), com campos consistentes como timestamp, level, message e service, o que facilita busca e correlação de eventos. Em ambientes maiores:
- Promtail é um agente que roda junto dos containers, lê os logs locais e os envia ao Loki. (Ele é o agente histórico do Loki, e a Grafana passou a indicar outro coletor; confirme na documentação da Grafana qual agente de coleta ela recomenda.)
- Loki armazena e consulta logs de forma eficiente, com um modelo semelhante ao do Prometheus, só que para logs.
- Fluentd pode coletar, transformar e rotear eventos, e serve em cenários mais complexos, em que os logs precisam ser enriquecidos antes do envio.
- Grafana consulta e visualiza os dados, monta dashboards e correlaciona logs com métricas.
Para começar, vale montar um setup simples com Compose que inclua o coletor, o Loki e o Grafana. Combinar logs estruturados com alertas sobre eventos críticos eleva a observabilidade da aplicação.
Os conceitos-chave
Healthcheck
verificação periódica de capacidade de serviço, e não de presença do processo.
starting, healthy, unhealthy
os três estados de saúde.
start_period
tolerância inicial para a aplicação subir.
stdout e stderr
onde a aplicação deve escrever seus logs.
Log estruturado (JSON)
campos fixos que permitem buscar e correlacionar.
Centralização
coletor, armazenamento e visualização (Promtail/Fluentd, Loki, Grafana).
Exemplos comentados
services:api:image: minha-api:1.0healthcheck:# Executa uma requisição com uma ferramenta presente na imagem.test: ["CMD", "curl", "--fail", "http://localhost:3000/health"]# Verifica a cada 30 segundos.interval: 30s# Interrompe uma tentativa que passe de 10 segundos.timeout: 10s# Marca unhealthy após três falhas consecutivas.retries: 3# Dá tempo para a aplicação iniciar antes de contar falhas.start_period: 20s
Garanta que curl existe na imagem. Em imagens mínimas, prefira uma verificação feita pelo próprio runtime da aplicação ou inclua deliberadamente a ferramenta necessária. Saída esperada em docker compose ps:
Comandos essenciais desta etapa
STATUS mostra (healthy), (unhealthy) ou (health: starting)docker run--follow, --since 5m, --timestamps: variações úteis do docker logsMão na massa
- Rode o exemplo do sensor e observe a sequência
starting,unhealthyehealthy. - Construa a imagem
minha-api:saude, suba o container e acompanhe oSTATUSemdocker psaté aparecer(healthy). - Rode
docker logs --tail 5 --timestamps api-saudee observe o horário de cada linha. Depois remova o container comdocker rm --force api-saude. Os exercícios de perder a dependência real de uma API e ver o healthcheck reagir ficam na Etapa 13, depois que você tiver a stack completa para testar.
Dica: Escolha o que o healthcheck testa conforme o que quebra de verdade. Um teste que só confere se a porta abre pode passar com o banco fora do ar; um que consulta o banco a cada segundo pode se tornar a maior fonte de carga. Prefira uma verificação leve que ainda dependa do que é essencial.
Anota aí: Processo ativo responde à pergunta "está rodando?"; healthcheck responde "consegue cumprir sua função?".
Etapa 13 stack defensiva
Capstone, uma stack pequena e defensiva
O problema
Os conceitos isolados funcionam nos exercícios, mas produção exige que eles cooperem. A imagem precisa ser reproduzível, o processo deve rodar sem root, a API precisa localizar o banco por nome, o banco precisa persistir dados, a senha não deve virar variável exposta e o ambiente deve dizer quando cada serviço está saudável.
O conceito
Este projeto final combina somente mecanismos já apresentados: Dockerfile, multistage build, usuário não root, rede personalizada, volume, tmpfs, segredo em arquivo, healthcheck, dependência por saúde, privilégios reduzidos, logs e Compose. Nenhum mecanismo isolado entrega uma aplicação confiável; o resultado vem da disciplina do conjunto.
A API usa apenas módulos nativos do Node. Ela testa se consegue abrir uma conexão TCP com o PostgreSQL. Isso prova descoberta por DNS e conectividade sem adicionar uma biblioteca de banco ao exercício.
O Compose também aplica limites simples de CPU e memória. Eles contêm o consumo máximo do laboratório, mas não garantem desempenho nem reservam capacidade exclusiva. Labels acrescentam metadados para inventário, automação e observabilidade; elas descrevem os serviços sem alterar seu comportamento.
O serviço api usa init: true: como a Etapa 03 explicou, o Node como PID 1 ignora o SIGTERM sem um tratador de sinal, e o init repassa o sinal, de modo que docker compose down e docker compose stop terminam em segundos em vez de esperar o prazo de 10 segundos.
O healthcheck do banco usa pg_isready -h 127.0.0.1, com o endereço TCP explícito. A imagem do PostgreSQL, na primeira inicialização, sobe um servidor temporário que escuta apenas em socket Unix enquanto prepara o banco. Sem o -h, o pg_isready consulta esse socket e pode responder "pronto" antes de o servidor definitivo aceitar conexões TCP, que é o caminho que a API usa. Com o endereço explícito, o teste só passa quando o banco realmente atende por TCP.
Checklist de produção. Antes de colocar qualquer container em produção, vale uma última revisão. O capstone cobre cada item abaixo, e o arquivo indica onde:
- Imagem otimizada: evite imagens genéricas e pesadas (como
node:latest); prefira variantesslim/alpineou um multistage, para que só o essencial vá para produção. Coberto peloDockerfile. - Variáveis sensíveis seguras: senhas e tokens via
secrets:, e não viaenv_fileou diretamente no Compose. Coberto pelo blocosecrets. - Limites de recursos: CPU e memória definidos para não sobrecarregar o host. Coberto por
cpusemem_limit. - Healthcheck configurado: serve de alerta e dá ao orquestrador o critério de reinício. Coberto pelos dois
healthcheck. - Log estruturado: a aplicação escreve JSON em stdout, pronto para uma stack de observabilidade (Etapa 12). Coberto pelo
server.js. - Usuário sem privilégios:
USER nodeno Dockerfile. Coberto. - Rede isolada: rede específica entre serviços, em vez da bridge padrão. Coberto pela rede
interna. - Persistência garantida: volume para que nenhum dado importante se perca em um restart. Coberto por
dados_db. - Labels e metadados: facilitam automação e integração com ferramentas de monitoramento. Coberto pelas
labels. - Teste final local: simule o ambiente de produção localmente, combinando os arquivos (
docker compose -f compose.yaml -f compose.prod.yaml up) antes de subir de verdade. Veja o último passo da seção "Mão na massa".
Os limites também podem ser escritos no formato deploy.resources.limits do Compose, que a bibliografia usa:
services:api:deploy:resources:limits:# Meia CPU e 512 MB de memória.cpus: "0.50"memory: 512M
Ambos os formatos são aceitos pelo Compose v2; este guia usa cpus e mem_limit. Confira o resultado com docker compose config.
interna, os dados ficam no volume e a senha chega como arquivo em /run/secrets/.Diagrama: feito para este guiaOs conceitos-chave
Stack
API, banco, rede, volume e segredo descritos juntos.
Dependência por saúde
depends_on com condition: service_healthy aguarda a prontidão, e não apenas a criação.
Privilégios reduzidos
cap_drop: ALL, read_only: true, tmpfs em /tmp, USER node.
init: true
um init como PID 1 repassa o SIGTERM ao Node e acelera o encerramento.
Segredo em arquivo
POSTGRES_PASSWORD_FILE aponta para /run/secrets/db_password.
Rede interna
DNS por nome de serviço, sem publicar o banco.
Limites de recurso
contêm o consumo, sem reservar capacidade.
Labels
metadado de inventário, e não controle de segurança.
pg_isready -h 127.0.0.1
verifica o banco pelo mesmo caminho TCP que a API usa.
Exemplos comentados
Estrutura:
capstone/├── app/│ └── server.js├── secrets/│ └── db_password.txt├── .dockerignore├── Dockerfile└── compose.yaml
app/server.js:
Comandos essenciais desta etapa
capstone_dados_db (prefixo = nome da pasta) continua existindodocker compose start db: param e retomam o banco, para testar a reação do healthcheck da APIMão na massa
- Crie a estrutura e os arquivos exatamente como mostrados.
- Grave uma senha de laboratório com
mkdir -p secrets && printf 'troque-esta-senha' > secrets/db_password.txt && chmod 644 secrets/db_password.txt. Ochmod 644importa porque o Postgres lê o arquivo como o usuáriopostgresdentro do container; com umaumaskrestritiva no host, o arquivo nasceria legível só por você e a leitura poderia falhar. - Valide o YAML com
docker compose config --quiet. - Suba a stack com
docker compose up --detach --build --wait. - Rode
curl http://localhost:3000/health. - A saída esperada é
{"service":"api","database":"reachable"}. - Rode
docker compose pse confirme que ambos os serviços estão saudáveis. - Rode
docker compose logs apie localize o log JSON da requisição. - Confirme o usuário com
docker compose exec api id. A saída deve identificar o usuárionode, nãoroot. - Execute
docker compose down, suba novamente e confirme que o volumecapstone_dados_db(o prefixo é o nome da sua pasta) continua existindo comdocker volume ls. Repare também que odowntermina em poucos segundos, graças aoinit: true. - Se quiser preservar o banco, encerre apenas com
docker compose down. Usedocker compose down --volumessomente quando decidir descartar o laboratório: esse comando remove o volumecapstone_dados_dbe apaga definitivamente seu conteúdo, sem lixeira ou recuperação automática. Faça backup antes se houver qualquer dado necessário. - Para fechar o item 10 do checklist de produção, escreva um
compose.prod.yamlmínimo (por exemplo, trocandorestartou adicionando uma labelambiente: producao), valide comdocker compose -f compose.yaml -f compose.prod.yaml confige suba com-fnos dois arquivos antes de considerar o ambiente pronto. - Extra, perder o banco e voltar: com a stack no ar (
docker compose up --detach --build --wait), confirme o estado inicial comdocker compose ps. API e banco devem aparecer comohealthy. - Extra: pare a dependência real da API com
docker compose stop db. - Extra: acompanhe
docker compose psaté a API aparecer comounhealthy. O endpoint retorna 503 porque o teste TCP ao banco falha, ecurl -i http://localhost:3000/healthmostraHTTP/1.1 503com{"service":"api","database":"unreachable"}. - Extra: consulte as tentativas com
docker inspect --format '{{json .State.Health}}' $(docker compose ps -q api)e correlacione comdocker compose logs api, onde as linhas de nívelerroraparecem. - Extra: recupere o banco com
docker compose start dbe aguarde os dois serviços voltarem ahealthyemdocker compose ps.
Dica: Depois de concluir o capstone, repita-o provocando uma falha de propósito: pare o banco, remova o volume, troque a senha. Se você consegue prever e explicar cada resposta do sistema (estado, healthcheck, logs), você entendeu como as peças funcionam juntas.
Anota aí: Uma stack madura não depende de um único recurso: ela combina construção reproduzível, isolamento, menor privilégio, persistência, saúde e evidências operacionais.
Etapa 14 · revisão
Checklist de autoavaliação
Etapa 01: Por que containers existem
- Consigo explicar por que container não é uma VM pequena.
- Sei dizer o papel de namespaces e cgroups.
- Consigo explicar por que o processo principal determina a vida do container.
- Vi o isolamento de PID e de hostname e li um limite de memória registrado em um container.
Etapa 02: Imagem, container e ciclo de vida
- Diferencio imagem, camada e container.
- Executei o ciclo
pull,run,stop,starterme confirmei que a imagem sobrevive ao container. - Sei explicar por que
lateste outras tags não garantem reprodutibilidade e o que um digest resolve. - Sei distinguir registry público de privado e descrever o fluxo
login,tagepush. - Usei
docker exec,docker cpedocker renameem um container em execução.
Etapa 03: Dockerfile
- Sei construir e versionar uma imagem com Dockerfile.
- Diferencio
RUN,CMD,EXPOSEe--publish. - Substituí o
CMDde uma imagem na linha de comando sem reconstruí-la. - Sei encerrar um container em primeiro plano e explicar por que o
--initajuda o Node como PID 1.
Etapa 04: Camadas, cache e multistage
- Sei organizar um Dockerfile para aproveitar cache.
- Observei
CACHEDe provoquei a invalidação do cache invertendo a ordem das instruções. - Entendo quando escolher
slim,alpine,distrolessou uma imagem mais completa. - Consigo explicar o benefício de um multistage build e o cuidado com as dependências de runtime.
Etapa 05: Persistência
- Sei escolher entre volume, bind mount e tmpfs.
- Comprovei que um dado em volume sobrevive à remoção do container e que um dado em tmpfs não sobrevive nem a um restart.
- Sei explicar o que muda ao usar um driver remoto (NFS ou plugin) em vez do
local.
Etapa 06: Redes
- Sei conectar serviços por nome em uma rede personalizada.
- Mostrei a diferença entre a bridge padrão e uma rede personalizada quanto à resolução de nomes.
- Publico somente as portas que precisam sair da rede interna.
- Sei dizer quando usar
host,noneeoverlay.
Etapa 07: Docker Compose
- Consigo validar, subir, observar e derrubar uma aplicação Compose.
- Sei explicar o que
depends_ongarante e o que não garante. - Sei usar
buildcomcontext,dockerfileeargs.
Etapa 08: Ambientes, reinício, escala e CI
- Sei combinar arquivos Compose para diferentes ambientes e conferir o resultado com
config. - Sei dizer quando o override é carregado automaticamente e quando não é.
- Entendo que restart não garante prontidão de dependências.
- Vi uma porta fixa impedir réplicas no mesmo host.
- Sei descrever um roteiro de CI que sobe, testa e derruba o ambiente.
Etapa 09: Segurança
- Consigo configurar usuário não root, filesystem somente leitura e capabilities mínimas.
- Provei o
--read-onlycomo root, comparando com e sem a opção. - Sei dizer o papel de capabilities, seccomp e menor privilégio.
- Sei resolver problemas de permissão em bind mounts com
--usere sei por que não montardocker.sock.
Etapa 10: Contexto, segredos e análise
- Sei por que
.enve segredos não devem entrar no contexto de build. - Vi o tamanho do contexto cair com o
.dockerignore. - Sei explicar por que apagar um segredo numa camada posterior não o remove da imagem, e extraí o segredo da camada com
docker save. - Diferencio
ARG,env_fileesecrets. - Sei para que servem Dive e Snyk.
Etapa 11: Troubleshooting
- Consigo investigar uma falha usando status, logs, inspect e events, nessa ordem.
- Sei interpretar os códigos de saída 0, 125, 126, 127 e 137 e verificar
OOMKilled, sabendo que o 137 também aparece quando umdocker stopesgota o prazo.
Etapa 12: Logs, healthcheck e prontidão
- Diferencio processo ativo de serviço saudável.
- Vi um container passar por
starting,unhealthyehealthy. - Consigo explicar como logs estruturados ajudam a centralização e a busca.
Etapa 13: Capstone
- Concluí o capstone e consigo justificar cada decisão do Compose.
- Mapeei cada item do checklist de produção para uma linha do arquivo.
- Parei o banco, vi a API ficar
unhealthye a vi voltar ahealthyquando o banco retornou.
Próximos passos: com todos os itens marcados, refaça os laboratórios sem consultar o guia e depois junte as peças. Provoque falhas no capstone (pare o banco, troque a senha, remova o volume) e explique cada resposta do sistema antes de olhar os logs. Em seguida, acrescente um terceiro serviço (por exemplo, um cache) e leve a mesma stack para um fluxo de CI com docker compose up --wait.
Notas do professor: precisões técnicas
- Este bloco reúne as correções feitas sobre o material bruto e os detalhes que ajudam a ler os exemplos com segurança. Nada aqui muda o caminho dos exercícios.
- Correções ao material bruto
- Root do container não é o root do host. O texto bruto afirma que o root dentro do container é "praticamente o mesmo" do host. Namespaces e capabilities reduzem seus poderes. Ainda assim, executar como UID 0 amplia o impacto de mounts perigosos, capabilities extras e falhas do runtime, e por isso o guia recomenda usuário não root.
- User namespace não é o padrão. O bruto apresenta o user namespace como se todo container tivesse IDs de usuário separados dos do host. Na configuração padrão do Docker, o UID 0 do container corresponde ao UID 0 do host (com capabilities reduzidas); o mapeamento só existe se
userns-remapou o modo rootless estiverem habilitados. É por isso que, sem--user, um bind mount cria arquivos pertencentes ao root do host (Etapa 09). - `tmpfs` não é driver de volume persistente. O bruto o lista entre os drivers de volume. É uma montagem em memória que perde o conteúdo quando removida.
- A bridge padrão não tem DNS por nome. O bruto diz que a rede bridge padrão fornece resolução de nomes. Essa resolução é uma vantagem das redes criadas pelo usuário; na bridge padrão, não dependa dela (Etapa 06).
- Overlay pertence ao Docker Swarm. O bruto associa as redes overlay ao Swarm ou ao Kubernetes. O Kubernetes não usa redes overlay do Docker; ele usa plugins de rede (CNI) próprios. O guia menciona overlay apenas como rede multi-host do Swarm.
- Google Container Registry. O serviço citado no bruto foi substituído pelo Artifact Registry; o guia usa o nome do serviço substituto.
- `docker-compose` com hífen. Os exemplos do bruto usam o executável da primeira geração. O guia usa
docker compose(Compose v2) em todo o texto. O nomedocker-compose.ymlcontinua aceito, mascompose.yamlé o nome preferido. - Exemplo de cache com pnpm. O bruto copia apenas
pnpm-lock.yamlantes depnpm install, mas o gerenciador também precisa dopackage.json. O guia copia os dois e usa--frozen-lockfile. - Ordem do `USER` no Dockerfile. O exemplo do bruto troca para o usuário comum antes de
RUN npm ci. Como o diretório criado porWORKDIRpode ter o root como dono, a instalação pode falhar por permissão. O guia instala as dependências antes deUSERe usaCOPY --chown. - Multistage e dependências. O bruto copia apenas
/app/distpara o runtime sem ressalva. Isso só funciona se o artefato for autossuficiente; caso contrário, a etapa final precisa denpm ci --omit=devpróprio. - Tamanho do Alpine. O bruto diz "menos de 10 MB" em uma aula e "cerca de 5 MB" em outra. O guia fala em "poucos megabytes", já que o valor varia entre versões.
- Cgroups e rede. O bruto inclui largura de banda de rede entre os recursos controlados por cgroups. O Docker expõe limites de CPU, memória e entrada e saída de disco por flags; o guia se restringe a esses.
- `env_file` e versionamento. O bruto sugere
env_filepara valores que "você precisa versionar". Arquivos.envcom credenciais não devem ser versionados, e valores deenv_fileficam visíveis emdocker inspect. - Promtail. O bruto o apresenta como o agente do Loki. Ele é o agente histórico, e a Grafana passou a indicar outro coletor; como essa recomendação pode mudar, o guia pede para confirmar na documentação qual agente ela indica.
- Erros de extração. Comandos como
dockerpull,dockerinspectedocker-composeupaparecem com espaços perdidos no bruto, e a opção-adedocker psaparece com um hífen tipográfico longo no lugar do hífen simples. O guia os restaura paradocker pull,docker inspect,docker ps -aedocker compose up. - Sobre o que você vai ver no terminal
- Saídas de exemplo. As saídas mostradas são formatos típicos. IDs, tempos, tamanhos, IPs e versões variam. Compare a estrutura, não o texto exato.
- Tags de imagem. As tags
lts-alpineelts-slimdo Node acompanham a versão LTS vigente, o que evita que os exercícios envelheçam, mas as torna móveis: em um projeto, fixe a versão principal. A imagempostgres:16-alpinedeve manter a versão principal escolhida, porque os arquivos de dados não são compatíveis entre versões principais. Confirme a disponibilidade das imagens de terceiros (comocurlimages/curl) no momento da execução. - Camada gravável e `docker cp`. Arquivos copiados para dentro de um container ficam na camada gravável e somem com ele; para persistir, use volumes.
- Limites de recurso.
cpusemem_limit(oudeploy.resources.limits) contêm o consumo, mas não reservam capacidade exclusiva nem substituem monitoramento. - Label é metadado, não controle de segurança. Ela ajuda inventário, automação e observabilidade, mas não impõe isolamento ou autorização.
- Segredos no Compose local.
secretsmonta o valor como arquivo e reduz a exposição, mas não é um cofre criptografado; o arquivo de origem continua precisando de proteção. - `ARG` não é segredo. Valores de build podem aparecer em histórico, metadados e registros de pipeline. Para segredos de build, existe um mecanismo próprio do BuildKit.
- PID 1, sinais e `--init`. No Linux, o PID 1 de um namespace de processos não recebe a ação padrão dos sinais: SIGTERM e SIGINT são ignorados se o programa não instalou um tratador. O Node.js sem tratador explícito é afetado, então o Ctrl+C pode não encerrar um container em primeiro plano e o
docker stopespera 10 segundos antes de enviar SIGKILL.--init(einit: trueno Compose) resolve isso com um init que repassa os sinais. Uma alternativa é tratar o sinal no código, com algo comoprocess.on("SIGTERM", () => server.close(() => process.exit(0))). - O 137 tem duas origens. O código de saída 137 significa SIGKILL. Ele aparece em falta de memória (com
OOMKilled=true) e também quando umdocker stopesgota o prazo e o Docker mata o processo (comOOMKilled=false). Quem leu "137" como falta de memória precisa conferir oOOMKilledantes de concluir. - tmpfs e swap. Um tmpfs vive na memória, mas, sob pressão de memória, o sistema pode mover suas páginas para a área de swap, e por isso ele não é uma garantia absoluta de que o dado nunca chega ao disco.
- Contexto de build incremental. O BuildKit transfere o contexto de forma incremental: na segunda execução de um build, só as diferenças são enviadas. Por isso o exercício de
.dockerignoreda Etapa 10 pede que o primeiro build, com o arquivo grande, seja feito uma única vez antes de criar o.dockerignore. - Tamanhos no `docker history`. Dependendo da versão e do image store usado pelo Docker (o clássico ou o do containerd), os tamanhos das camadas podem aparecer arredondados ou em outra unidade. A leitura que importa é a comparação: a camada do
COPYé maior que0Be a doRUN rmcontinua em0B. - `pg_isready` e o servidor temporário. Na primeira inicialização, a imagem oficial do PostgreSQL executa um servidor temporário só em socket Unix, reinicia e então passa a escutar em TCP. Um
pg_isreadysem-hpode acusar prontidão nessa fase; com-h 127.0.0.1, o teste reflete o caminho TCP usado pela API. - Pontos a verificar por quem for executar
- Docker Desktop. Os containers Linux rodam dentro de uma VM. O modo de rede
hoste a tradução de donos de arquivos em bind mounts se comportam de forma diferente do Linux nativo (Etapas 06 e 09). - Reinício e contagem. O número de reinícios mostrado na Etapa 08 (
reinicios=3) e o número de linhas de log dependem do tempo em que você consulta; aguarde alguns segundos antes de inspecionar. - Serviços externos. O
docker push(Etapa 02) exige um registry real, o volume NFS (Etapa 05) exige um servidor NFS acessível, o Snyk (Etapa 10) exige conta, e o Dive exige instalação. Os demais exercícios não dependem deles. - Portas em uso. Os exemplos usam as portas 3000 e 8080 do host. Se alguma estiver ocupada, altere o lado esquerdo do mapeamento (por exemplo,
3001:3000) e ajuste ocurl. - Imagens mínimas e diagnóstico. Alpine, slim e distroless reduzem tamanho e superfície, mas compatibilidade e capacidade de diagnóstico também precisam ser consideradas; uma imagem sem shell não aceita
docker exec ... sh. - Extração de camadas com `docker save`. O formato do arquivo exportado muda conforme a versão do Docker (camadas em diretórios próprios ou em
blobs/, com ou sem compressão). O roteiro da Etapa 10 percorre todos os arquivos justamente para não depender desse layout; se otarnão encontrar o caminho, tente a variante./tmp/segredo.txt.