Docker para iniciantes: imagens reproduzíveis e containers operáveis

Empacotar a aplicação é apenas o começo. Um container precisa iniciar, parar, expor saúde, preservar dados e receber configuração sem improviso.

Um container é um processo, não uma máquina pequena Uma imagem contém o sistema de arquivos e os metadados necessários para iniciar uma aplicação. Um container é uma instância em execução dessa imagem. Os containers compartilham o kernel do host. Eles não são máquinas virtuais completas e não criam uma barreira absoluta de segurança. Essa distinção muda o modo de operar: o processo principal precisa permanecer em primeiro plano; logs devem sair para stdout e stderr ; dados persistentes não devem depender da camada gravável; configuração deve chegar externamente; parada precisa ser tratada; saúde precisa ser observável; imagem precisa ser reconstruída para receber código novo. O laboratório Crie um diretório vazio: Adicione app.py : A aplicação: lê configuração do ambiente; responde em / ; oferece /health ; escreve logs estruturados; recebe sinais; fica em primeiro plano. Ela é didática. Para produção, use um servidor e framework compatíveis com a carga e os requisitos do projeto. O Dockerfile mais curto não é necessariamente o melhor Crie Dockerfile : Base image python:3.14 slim é uma referência legível, mas tags podem mudar. Para produção, considere fixar digest: O digest garante exatamente qual imagem foi usada. Ele também exige processo de atualização para não congelar vulnerabilidades. Usuário sem privilégios O USER evita iniciar a aplicação como root dentro do container. Isso não substitui: isolamento do host; seccomp; AppArmor ou SELinux; capabilities; atualização; configuração do daemon. Ainda reduz o impacto de determinadas falhas. Forma exec Use: em vez de: A forma exec evita uma camada de shell desnecessária e melhora o recebimento de sinais pelo processo principal. Construa a imagem pull solicita uma versão atual da base correspondente à referência. Isso não torna o build reproduzível se a tag for mutável. Inspecione: O histórico mostra as camadas criadas pelas instruções. Execute com exposição local A publicação usa 127.0.0.1 . Assim, a porta fica disponível apenas na máquina local. Isto: normalmente publica em todas as interfaces do host. Em um servidor, isso pode expor o serviço além do esperado. Teste: Em outro terminal: Interrompa com Ctrl+C . O processo recebe o sinal e encerra. Imagem e configuração precisam permanecer separadas Não grave isto no Dockerfile: A informação pode permanecer em camadas, histórico ou metadados. Variáveis são adequadas para configurações não sensíveis: Segredos devem ser fornecidos por mecanismo apropriado ao ambiente: arquivo montado; secret store; Docker Swarm secrets; Kubernetes Secrets combinados com proteção adicional; identidade de workload; cofre externo. A aplicação deve saber ler o segredo sem imprimi lo. Dados persistentes não pertencem à camada do container Quando o container é removido, sua camada gravável desaparece. Para dados persistentes, use: banco externo; object storage; volume; serviço gerenciado; mecanismo específico da aplicação. Exemplo de volume: Antes de montar, confirme: UID e GID; backup; retenção; criptografia; restauração; concorrência; permissões. Montar um diretório do host oferece flexibilidade, mas acopla o container à estrutura local. .dockerignore reduz contexto e vazamento Crie .dockerignore : O contexto de build é enviado ao builder. Excluir arquivos: acelera transferência; reduz invalidações; diminui risco de copiar segredos; evita conteúdo irrelevante. Não confie somente no .dockerignore para proteger segredos. Use varredura e políticas. Ordem das instruções altera o cache Considere uma aplicação Python com dependências. Ineficiente: Qualquer alteração invalida a camada de instalação. Melhor: A documentação de cache do Docker recomenda ordenar instruções do menos alterado para o mais alterado e reduzir o contexto. BuildKit também suporta cache mounts para gerenciadores de pacote, sem copiar esse cache para a imagem final. Multi stage separa construção e execução A documentação oficial recomenda stages para manter compiladores e dependências de build fora da imagem final. Estrutura genérica: O resultado final contém apenas o necessário para executar. Não escolha uma imagem mínima que torne impossível: aplicar certificados; resolver DNS; depurar incidentes; executar healthcheck; receber atualizações. Tamanho é um critério, não o único. Um container deve fazer pouco, mas não por dogma Separar aplicação, banco e proxy costuma melhorar: atualização; escala; observação; isolamento; responsabilidade. Isso não significa criar um container para cada função interna da aplicação. Um worker pode ser a mesma imagem com outro comando: O artefato permanece comum e o papel muda por configuração. Docker Compose descreve um conjunto local Exemplo: Execute: Em outro terminal: Encerre: Esse comando remove os containers e a rede criados pelo projeto. Volumes nomeados não são removidos sem opção específica. Ordem de início não significa prontidão O Compose pode iniciar o banco antes da aplicação, mas o processo do banco pode ainda não aceitar conexões. A documentação de startup order permite combinar depends on e service healthy . Ainda assim, a aplicação precisa: usar timeout; repetir conexões; limitar tentativas; registrar o erro; não entrar em loop agressivo; recuperar quando a dependência retornar. Orquestração não substitui resiliência no código. Healthcheck precisa testar a condição correta Um healthcheck que apenas verifica o PID pode considerar saudável uma aplicação travada. Separe conceitos: Liveness O processo está funcionando o suficiente para continuar? Readiness A instância pode receber tráfego? Dependência externa Banco ou fila estão acessíveis? Não transforme toda indisponibilidade externa em liveness failure. Reiniciar a aplicação repetidamente porque o banco está fora pode piorar o incidente. O Docker possui um healthcheck por container. Plataformas de orquestração podem oferecer sinais separados. Logs devem sair do processo Evite depender apenas de arquivos internos: Prefira: O runtime coleta e encaminha. Logs precisam incluir contexto: Não registre: senha; token; cookie; corpo completo; dados pessoais sem necessidade; chave privada. A observabilidade começa na aplicação, não no dashboard. Limites evitam que um processo consuma o host inteiro Sem limites, um container pode utilizar recursos disponíveis do host. Defina conforme medição: Limites incorretos também causam falhas. Observe: memória real; garbage collection; picos; threads; arquivos; sockets; latência sob pressão. Não monte o socket do Docker sem compreender o risco Isto é sensível: Quem controla o socket pode controlar o daemon e, frequentemente, o host. Evite montá lo em: aplicações; dashboards não confiáveis; runners compartilhados; containers expostos. Quando uma ferramenta realmente precisa, aplique isolamento, proxy de API e permissões mínimas. Rootless reduz o privilégio do daemon O modo rootless do Docker executa daemon e containers sem privilégios root no host. Ele pode reduzir impacto de falhas, mas possui: requisitos de sistema; limitações de rede; diferenças de recursos; considerações operacionais. Teste compatibilidade antes de adotar. Pipeline mínimo da imagem O CI/CD seguro deve construir uma vez e promover a mesma imagem. Checklist antes de publicar [ ] Base mantida e fixada conscientemente. [ ] Contexto reduzido. [ ] Dependências determinísticas. [ ] Somente arquivos necessários. [ ] Processo sem root. [ ] CMD ou ENTRYPOINT em forma exec. [ ] Sinais tratados. [ ] Configuração externa. [ ] Segredos fora da imagem. [ ] Dados persistentes fora da camada gravável. [ ] Healthcheck adequado. [ ] Logs em stdout e stderr . [ ] Limites definidos. [ ] Porta publicada conscientemente. [ ] Imagem testada e analisada. [ ] Atualização e rollback planejados. Docker facilita empacotar um ambiente, mas não garante sozinho reprodutibilidade absoluta, segurança ou boa operação. A imagem precisa ser identificável; o container, descartável; os dados, recuperáveis; e o host, protegido pelo hardening de Linux. Esse é o ponto em que “funciona no container” se transforma em “pode ser operado”.