Docker do jeito certo: imagens enxutas, multi-stage e segurança de containers

Boas práticas de Docker que fazem diferença em produção: builds multi-stage, imagens enxutas, camadas eficientes e segurança de containers.

Equipe Solvefy 8 min de leitura

Colocar uma aplicação dentro de um container é fácil. Fazer isso de um jeito que seja rápido, leve e seguro em produção é onde a maioria escorrega. Uma imagem mal construída é lenta para subir, cara para armazenar e, pior, uma porta aberta para problemas de segurança. Veja o que separa o Docker de exemplo de tutorial do Docker de produção.

Imagens enxutas: por que o tamanho importa

Imagem grande é imagem lenta — para baixar, para subir, para escalar. E cada megabyte a mais é superfície de ataque a mais. Alguns princípios:

  • Parta de uma base mínima. Imagens slim ou alpine cortam centenas de megabytes de coisas que a sua aplicação não usa.
  • Instale só o necessário. Ferramentas de build, compiladores e caches não precisam viver na imagem final.
  • Limpe no mesmo passo. Baixar e remover em camadas diferentes não reduz o tamanho — a camada anterior fica no histórico. Faça a limpeza no mesmo RUN.

Há um ganho colateral que aparece na escala: imagem pequena significa provisionamento rápido. Em orquestrador que sobe e derruba réplicas conforme a carga, a diferença entre baixar 80 MB e 800 MB aparece direto no tempo de resposta a um pico de tráfego.

Escolhendo a base sem se arrepender

Cada família de base resolve um problema e cobra um preço:

BaseVantagemCusto a considerar
Completa (padrão da distribuição)Tudo funciona, fácil de depurarGrande, muitos pacotes a manter
slimBom equilíbrio, compatibilidade preservadaFalta ferramenta ocasional no build
alpineMuito pequenaUsa musl em vez de glibc; algumas dependências exigem compilação
Distroless / mínima sem shellSuperfície de ataque mínimaSem shell para depurar dentro do container

O caso do Alpine merece nota, porque é a escolha que mais gera surpresa. A biblioteca C diferente faz com que pacotes binários prontos de algumas linguagens não sirvam, obrigando a compilar na hora do build — e um build que era rápido passa a levar minutos. Para muitas stacks, a variante slim da distribuição padrão entrega quase o mesmo tamanho sem esse atrito.

Multi-stage build: o pulo do gato

O build multi-stage é a técnica que mais reduz imagem sem esforço. A ideia: use um estágio "pesado" para compilar/construir a aplicação, e copie apenas o resultado para um estágio final enxuto. Compiladores, dependências de build e arquivos intermediários ficam para trás.

O ganho é duplo: imagem final pequena e uma separação limpa entre o que é necessário para construir e o que é necessário para rodar. Uma aplicação que resultaria em uma imagem de centenas de MB pode terminar em dezenas.

Um desenho de estágios que funciona bem na prática:

  1. Estágio de dependências: instala bibliotecas a partir dos arquivos de manifesto, aproveitando cache entre builds.
  2. Estágio de build: compila, empacota ou gera os artefatos estáticos.
  3. Estágio de teste (opcional): roda a suíte sobre o artefato construído, falhando o build quando quebra.
  4. Estágio final: parte de uma base mínima e copia apenas o artefato e os arquivos de runtime.

Como bônus, dá para apontar o build para um estágio intermediário durante o desenvolvimento, tendo ferramentas de depuração à mão sem carregá-las para produção.

O arquivo que quase todo mundo esquece

Sem um .dockerignore bem escrito, o contexto de build inclui o diretório inteiro — histórico do repositório, dependências locais, artefatos de build anteriores, arquivos de configuração com credenciais. Isso deixa o build lento, invalida cache sem motivo e, no pior caso, coloca segredo dentro da imagem sem ninguém perceber.

Comece ignorando o controle de versão, as dependências instaladas localmente, diretórios de build, arquivos de ambiente e qualquer coisa que não seja necessária para construir. É o ajuste de cinco minutos com melhor retorno em todo o Dockerfile.

Camadas eficientes e cache

O Docker constrói em camadas e reaproveita cache. Ordene o Dockerfile do que muda menos para o que muda mais: copie e instale dependências antes de copiar o código-fonte. Assim, mudar uma linha de código não invalida o cache da instalação de dependências — e o build fica muito mais rápido.

No pipeline de CI, garanta que o cache seja realmente aproveitado entre execuções — seja reutilizando camadas do registry, seja com o mecanismo de cache do próprio construtor. Build de dez minutos que poderia levar um é atrito diário que desestimula entregas pequenas.

Como o processo se comporta dentro do container

Aqui está uma classe de problema que só aparece em produção. O processo principal do container roda como PID 1, e isso tem consequências:

  • Sinais. Se o comando é executado através de um shell, o sinal de encerramento pode não chegar à aplicação, que então é morta à força depois do tempo de espera. O resultado é conexão cortada no meio e transação interrompida a cada deploy. Use a forma de execução direta, sem shell intermediário.
  • Processos zumbis. PID 1 tem a obrigação de recolher processos filhos encerrados. Aplicações que geram subprocessos precisam de um init mínimo para isso — o Docker oferece essa opção nativamente.
  • Encerramento gracioso. A aplicação deve tratar o sinal de término: parar de aceitar novas requisições, terminar o que está em andamento e sair. Isso é o que permite deploy sem erro visível ao usuário.
  • Logs no stdout. Container não é lugar para escrever arquivo de log. Envie para a saída padrão e deixe a plataforma coletar, rotacionar e centralizar.
  • Healthcheck que reflete a realidade. Um endpoint que responde apenas "estou de pé" não ajuda; verifique as dependências essenciais, sem transformar o healthcheck em teste caro que derruba o serviço sob carga.

Segurança: o item que ninguém prioriza até o incidente

Container não é máquina virtual, e tratá-lo como se fosse cria riscos:

  • Não rode como root. Defina um usuário sem privilégio na imagem. Container comprometido rodando como root é um problema muito maior.
  • Fixe versões. latest é imprevisível — a imagem que subiu hoje pode não ser a de amanhã. Fixe tags e digests.
  • Escaneie as imagens. Ferramentas de scan encontram vulnerabilidades conhecidas nas dependências antes de irem para produção.
  • Não coloque segredo na imagem. Senha, chave e token vão via variável de ambiente ou gerenciador de segredos — nunca dentro do Dockerfile ou da imagem.
  • Minimize a superfície. Menos pacotes na imagem, menos vulnerabilidades para gerenciar.

Na hora de executar, quatro ajustes elevam bastante o patamar e custam pouco:

  • Sistema de arquivos somente leitura, com volumes explícitos apenas onde a aplicação precisa escrever.
  • Remoção de capacidades que a aplicação não usa, mantendo o mínimo necessário.
  • Bloqueio de escalada de privilégio, impedindo que o processo ganhe permissões durante a execução.
  • Limites de CPU e memória definidos. Sem limite, um container com vazamento de memória derruba o host inteiro, não apenas a si mesmo.

E uma regra sem exceção: não monte o socket do Docker dentro de um container que não seja de infraestrutura confiável. Acesso ao socket equivale a acesso root no host — é a porta dos fundos mais comum em ambientes que pareciam seguros.

Segredos: onde eles devem viver

Segredo em variável de ambiente é o padrão mínimo aceitável, e ainda vaza com facilidade — aparece em log de inspeção, em despejo de erro, no histórico de shell. O caminho melhor é injetar em tempo de execução a partir de um gerenciador de segredos, com rotação possível e registro de quem acessou. No build, use o mecanismo próprio de segredos do construtor, que não persiste o valor em nenhuma camada da imagem.

Registry e política de tag

Imagem só vale em produção se você souber exatamente o que está rodando. Duas práticas resolvem:

  • Tag imutável por versão, gerada no pipeline — um identificador de build ou o hash do commit. latest serve para conveniência local, nunca para produção.
  • Referência por digest no manifesto de deploy quando a garantia precisa ser absoluta.

Some a isso a limpeza periódica do registry, ou o custo de armazenamento cresce em silêncio, e a política de retenção passa a ser decidida pelo disco cheio.

Compose para orquestração local

Para desenvolvimento e ambientes simples, o Docker Compose organiza múltiplos containers (aplicação, banco, cache) em um arquivo declarativo. É a ponte natural antes de decidir se a escala justifica um orquestrador completo como Kubernetes.

Mantenha o Compose de desenvolvimento próximo da realidade de produção nas versões de dependência, mas separado nos ajustes de conveniência. Ambiente local que difere muito do destino final é fábrica de "na minha máquina funciona".

Checklist de imagem pronta para produção

  1. .dockerignore cobrindo tudo que não é necessário ao build.
  2. Build multi-stage, com imagem final sem ferramentas de compilação.
  3. Base fixada por tag específica, revisada periodicamente.
  4. Usuário sem privilégio definido na imagem.
  5. Nenhum segredo em camada, argumento de build ou código.
  6. Encerramento gracioso testado, com sinal chegando à aplicação.
  7. Healthcheck implementado e verificado.
  8. Logs na saída padrão.
  9. Limites de CPU e memória definidos na execução.
  10. Escaneamento de vulnerabilidade no pipeline, com política clara para o que bloqueia a entrega.

Onde a Solvefy/Cloud entra

Nossa consultoria DevOps trabalha conteinerização do jeito que aguenta produção: builds multi-stage, imagens enxutas, redes, volumes, Compose, integração com CI/CD e segurança de imagens — com documentação e transferência de conhecimento. Cobrada por hora, com escopo definido.

Quer revisar seus containers? Diagnóstico gratuito, sem compromisso, em solvefy.cloud.

Solvefy/Cloud — Infraestrutura que cresce com você.

Faça agora o seu diagnóstico e orçamento

Inicie com um diagnóstico gratuito da sua infraestrutura e receba uma proposta personalizada — com escopo, cronograma e valores.