Esquece o tutorial básico do Dockerfile
O projeto de um container começa muito antes de você escrever a primeira linha de instruções no Dockerfile. A maioria das pessoas pula essa etapa e cai nos mesmos problemas que eu já vi acontecerem semana após semana em projetos de produção.O projeto de um container na prática
O problema real não é criar um container que funcione. Funciona em qualquer máquina nova. O problema é o que acontece seis meses depois, quando você precisa fazer update, escalar, ou resolver um bug que só aparece com carga real. Na minha experiência, o erro mais comum é tratar o container como uma caixa preta isolada. Container não é caixa preta. É um processo com namespace e cgroup, rodando dentro de um sistema operacional real. Isso significa que ele herda limites, sofre com filesystem compartilhado, e tem comportamento dependente do kernel host. Ignorar isso gera dores de cabeça desnecessárias.
Vou te mostrar como estruturar isso de forma que não precise refazer tudo a cada trimestre.
Primeira decisão: imagem base e seu manutenção
A escolha da imagem base define 80% do que vai te dar trabalho depois. Alpinoy é leve, mas usa musl libc. Isso quebra bibliotecas compiladas contra glibc sem aviso prévio. Já perdi dois dias investigando um erro de link dinâmico em uma aplicação Python que funcionava perfeitamente no meu ambiente de desenvolvimento porque o PyPI já vinha com wheel pré-compilado para glibc. Para production, prefiro Debian Slim ou Ubuntu Minimal. O tamanho extra compensa pela estabilidade. Se o objetivo for realmente reduzir superfície de ataque e o tamanho for crítico, aí sim o Alpine faz sentido, desde que você entenda as implicações de musl e teste cada dependência binária.
Estrutura de camadas e cache do build
O Docker constrói imagens em camadas e cada linha do Dockerfile cria uma nova camada. Copiar arquivos grandes no início do Dockerfile destrói o cache. Sempre coloque COPY ou ADD de arquivos grandes por último. Um padrão que uso consistentemente é:
Copiar primeiro apenas requirements.txt ou equivalentes. Executar a instalação de dependências.
Copiar o resto do código-fonte. Isso garante que mudanças no código não invalidem o cache da instalação de dependências. No meu caso, isso reduziu o tempo de build de cerca de 4 minutos para aproximadamente 20 segundos quando só havia alterações nos arquivos de origem.
Outro detalhe que muitos ignoram: o tamanho da camada importa tanto quanto o número de camadas. Uma instrução RUN que instala pacotes e depois limpa o cache do package manager numa única linha reduz o tamanho final consideravelmente. Duas linhas diferentes criam duas camadas e o cache permanece na imagem mesmo após a limpeza.
👉 Clique no botão abaixo para saber mais sobre o assunto!
Segurança básica que ninguém cobre em tutorials
Rodar como root dentro do container é um erro comum. Criar um usuário não-root e switching com USER é trivial, mas tem um efeito colateral importante: permission errors em diretórios que o build system precisa acessar antes do switch. A solução é garantir que todos os arquivos copiados tenham dono e permissões adequados antes da instrução USER. Não exponha portas desnecessariamente. Se sua aplicação só precisa ouvir na loopback, use 127.0.0.1:porta no binding. Isso evita que serviços fiquem acessíveis pela rede do host acidentalmente.
Evite imagens multi-stage apenas para reduzir tamanho se não houver motivo real. Multi-stage builds são úteis, mas adicionam complexidade ao debugging. Use quando o tamanho final for crítico ou quando o builder tiver dependências que não pertencem ao runtime.
Network e persistência de dados
Container é efêmero por definição. Qualquer dado que precisa sobreviver ao rebuild ou restart deve viver em volumes externos. Não armazene arquivos de estado, logs, ou uploads dentro do container. Já vi equipes perderem semanas de dados porque alguém não configurou volume adequado e decidiu reconstruir a imagem sem backup. Para networking, defina redes customizadas em vez de usar a rede default bridge. A rede default não oferece resolução DNS automática entre containers. Com uma rede customizada, os containers se descobrem pelo nome do container. Isso elimina a necessidade de hardcoding de IPs, que mudam a cada restart de qualquer forma.
Um caso real que aprendi da pior forma
Trabalhei num projeto onde o container de aplicação precisava de acesso a hardware específico — uma placa de aquisição de sinais via USB. A abordagem ingênua seria mapear o dispositivo com --device. Funcionou no desenvolvimento. Quebrou em produção porque o driver da placa precisava de permissoes UDEV que não estavam presentes no container, e o runtime do Docker não carrega regras UDEV do host. A solução foi criar uma rede customizada e expor o serviço de aquisição através de uma API REST em vez de acesso direto ao dispositivo. Adicionou uma camada de complexidade, mas eliminou a dependência do hardware no container. Se você precisa de acesso direto a dispositivos, considere usar um sidecar container dedicado para isso, mantendo a lógica de negócio isolada.
Healthcheck e orquestração
Definir HEALTHCHECK no Dockerfile é importante, mas a maioria das implementações que vejo são inúteis. Um healthcheck que só verifica se o processo está rodando não detecta freezes de aplicação ou degradação de performance. O healthcheck deve fazer uma requisição real ao endpoint de status da aplicação e validar o response. Configure start-period para dar tempo à aplicação de inicializar, especialmente se ela carrega modelos ou conecta a bancos de dados. Sem isso, o orquestrador mata e reinicia o container continuamente durante o boot, criando um loop infinito.
O que funciona e o que não funciona
Containerização funciona bem para serviços stateless, APIs, workers de fila, e microserviços com dependências bem definidas. Não funciona bem para bancos de dados com dados críticos sem volumes mapeados, aplicações com forte acoplamento a hardware, ou cargas que exigem latência extremamente baixa entre componentes. Se o seu cenário cai em alguma dessas categorias, considere VMs tradicionais ou bare metal. Container adiciona complexidade de orquestração, logging distribuído, e troubleshooting de rede que pode não valer a pena dependendo do caso.
O projeto de um container eficiente exige pensar no ciclo de vida completo, não apenas no build. A imagem que você constrói hoje vai precisar ser atualizada, monitorada, e possivelmente substituída dentro de alguns meses. Escrever Dockerfiles reutilizáveis, com variables e argumentos construtíveis, economiza horas de refatoração posterior. Variáveis de build ARG permitem personalizar a imagem sem duplicar Dockerfiles inteiros para cada ambiente. A documentação oficial do Docker é útil mas foca no básico. Para tópicos avançados como security contexts, seccomp profiles, e resource limiting fino, a documentação técnica do Linux kernel é mais relevante do que você imaginaria. Entender cgroups, namespaces, ecapabilities do Linux faz diferença direta na qualidade do container que você entrega.