Novo: Eficify One em beta aberto. Crie seu primeiro ambiente sem cartão.Conhecer a plataforma →

Docker em produção: o que o tutorial não ensina

Container Docker flutuando sobre infraestrutura de cloud com múltiplas camadas de proteção visualizadas como escudos, conceito de produção e segurança
CompartilharSeguir

Todo mundo consegue escrever um Dockerfile. Mas a diferença entre uma imagem que passa no tutorial e uma que funciona em produção não é sorte: é engenharia deliberada. Depois de ver incontáveis pipelines quebrando por causa de secrets expostos, pods OOMKilled por falta de limites, imagens de 1.2 GB que demoram 4 minutos para fazer pull, percebi que o problema nunca é o conceito de Docker. É a distância entre saber fazer funcionar e saber fazer direito. Este artigo documenta essa distância com profundidade técnica real.

A maioria dos Dockerfiles que eu vejo em produção tem um cheiro familiar: imagem base ubuntu ou alpine genérica, tudo rodando como root, sem healthcheck, sem limites de recurso, COPY do código inteiro incluindo node_modules ou virtualenv. Funciona na máquina do dev. Em produção, vira problema.

O fosso entre tutorial e produção

As diferenças não são cosméticas. Elas determinam se sua imagem vai vazar secrets, vai travar em um loop de restart, vai demorar 3 minutos para fazer deploy, ou vai ser explorado por um exploit público. Vou destrinchar cada ponto crítico com código real e trade-offs concretos.

Antes de entrar nos detalhes, preciso de um framing. A tabela abaixo sintetiza o que separa uma imagem de dev de uma imagem de produção em cinco dimensões críticas.

Aspecto Dockerfile de tutorial Imagem de produção
Tamanho da imagem Base genérica (ubuntu, debian) com tudo instalado Slim image com apenas o runtime necessário
Usuário root (padrão) Usuário não-root com uid fixo
Healthcheck Ausente Configurado e testado
Recursos Sem limites CPU e memória declarados
Segredos Hardcoded ou via build args Mountados em runtime via secrets
Tags latest ou v1.0.0 Digest SHA256 imutável

Essa tabela é o mapa. O resto do artigo é a navegação.

Multi-stage builds: menos superfície de ataque

Builds multi-stage existem há anos, mas ainda vejo projetos ignorando-os. A ideia é simples: você usa uma imagem de build para compilar, e outra imagem, mínima, para empacotar apenas o artefato final.


FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production && npm run build


FROM node:20-alpine
# Não copie package.json inteiro, não instale devDeps
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
USER node
CMD ["node", "dist/index.js"]

Resultado concreto: uma imagem Node que pesava 1.1 GB cai para 180 MB. Build time cai pela metade em pipelines com cache frio. Mais importante: você não carrega compiladores, ferramentas de build nem código fonte na imagem final. Isso elimina uma categoria inteira de CVEs potenciais.

O trade-off é a complexidade de setup inicial e a necessidade de garantir que o artefato compilado seja portável (não dependa de libs nativas do estagio de build). Para linguagens interpretadas como Python ou Ruby, o ganho é menor, mas ainda existe se você usar virtualenvs limpos.

Usuário não-root: o mínimo que você deveria fazer

Container rodando como root é o padrão, e é também o maior risco de segurança que você provavelmente tem em produção hoje. Se um exploit no seu app der shell no container, esse shell é root no host, dependendo do driver de storage e das capabilities concedidas.

# Crie um usuário com uid fixo para reproducibilidade
RUN addgroup --system --gid 1001 appgroup && \
    adduser --system --uid 1001 --gid 1001 --shell /bin/false appuser

USER appuser

Uid fixo importa porque facilita reproducibilidade em clusters onde volumes e permissões são compartilhados. Se você usar uid aleatório, qualquer volume mountado pode dar permissão negada em tempo de deploy.

Um container rodando como root é um container que ainda não foi explorado.

Para workloads que genuinamente precisam de capabilities específicas (ex.: NGINX rodando na porta 80), use capabilities drop por padrão e adicione apenas o mínimo necessário:

securityContext:
  runAsNonRoot: true
  runAsUser: 1001
  capabilities:
    drop:
      - ALL
    add:
      - NET_BIND_SERVICE

Healthcheck: o guardião do restart automático

Sem healthcheck, o orchestrator não sabe quando o seu container está doente. Ele vai continuar mandando tráfego para um processo que está em deadlock, em loop de reconexão, ou simplesmente morto. O resultado é 502s em cascata.

HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
  CMD curl -f http://localhost:8080/health || exit 1

Para imagens sem curl (que é o caso de alpine slim), instale o wget no healthcheck ou use uma abordagem baseada em arquivo:

HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
  CMD wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1

O --start-period é crítico. Ele dá tempo para aplicações com initialization (banco de dados, caches, migrations) subirem antes do healthcheck começar a contar falhas. Sem ele, containers jovens são marcados como unhealthy e killed antes de ficarem prontos.

Em Kubernetes, o healthcheck do Dockerfile vira o readinessProbe. Para livenessProbe, você precisa de um endpoint separado que indique se o processo está vivo, mesmo que não esteja pronto para receber tráfego.

Limites de CPU e memória: previsibilidade em produção

Sem limites, um container faminto vai consumir tudo e fazer o vizinho pagar. Em Kubernetes, isso vira pod eviction, degradação de performance ou, no pior caso, nó inteiro caindo.

resources:
  requests:
    memory: "128Mi"
    cpu: "100m"
  limits:
    memory: "256Mi"
    cpu: "500m"

Estratégia para descobrir os limites certos: faça load test com o P95 de uso real, depois multiplique por 1.5 para cabeça. Memory limits devem ser suficientes para peak + headroom, porque container em OOM é killed sem graceful shutdown.

Trade-off: limits muito apertados causam throttling de CPU e OOMKilled. Limits muito soltos desperdiçam capacidade e aumentam custo. A calibragem correta é um exercício de medição, não de intuição.

Gestão de secrets: construindo não é runtime

Secrets no Dockerfile (via ARG ou ENV) são visíveis em todas as camadas intermediárias. Qualquer pessoa com acesso à imagem pode extrair o histórico de camadas e ver secrets em texto puro.

# ERRADO: secret vaza para a camada de imagem
ARG DB_PASSWORD
ENV DB_PASSWORD=${DB_PASSWORD}

# CERTO: secret montada em runtime, não construída
CMD ["node", "dist/index.js"]
# O orchestrator injeta via secret mount em /run/secrets/db_password

Em Kubernetes, use Kubernetes Secrets com atenção: por padrão, eles são base64 encoded, não criptografados. Para ambientes com compliance real, use um operator como External Secrets com integração a AWS Secrets Manager, HashiCorp Vault ou Azure Key Vault.

Tags e digest: imutabilidade que evita desastres

Tag latest é um antipattern em produção. Quando você faz docker pull myapp:latest duas vezes em dias diferentes, pode estar pegando imagens diferentes. Deploys não são reproduzíveis.

# Builds com digest SHA256 (imutável)
docker build -t myapp:$(git rev-parse --short HEAD) .
docker build --build-arg IMAGE_DIGEST=$(docker inspect myapp:latest | jq '.[0].RepoDigests[0]')

# Em produção, referencie por digest, não por tag
docker run myapp@sha256:a3f5e2c8d9...

Em Kubernetes, você pode usar ImagePullPolicy Always para forçar revalidate a cada pull, mas o digest é a única forma de garantir que o que você deployou ontem é o que está rodando hoje.

Camadas de segurança que você deveria estar usando

Além do básico, aqui vai o checklist que separa uma imagem razoável de uma imagem auditável:

  • Scan de vulnerabilidades no CI: Trivy, Snyk ou Grype devem falhar o pipeline se encontrarem CVEs críticos
  • Minimal base image: Distroless, Wolfi ou imagens oficiais com sufixo -slim reduzem superfície
  • UID range restrito: No Kubernetes, configure runAsUser e runAsGroup no securityContext do Pod
  • ReadOnly root filesystem: Impede que malware persista modificações no container
  • No privileged mode: Containers privilegiados podem acessar dispositivos do host diretamente

Conclusão: engenharia deliberada, não acidente

Uma imagem de produção não é um Dockerfile com mais linhas. É uma decisão consciente sobre cada superfície de ataque, cada ponto de falha, cada custo de operação. Builds multi-stage cortam tamanho e vulnerabilidades. Usuário não-root limita o blast radius de exploits. Healthcheck e limites transformam o orchestrator em aliado, não em ignorante. Secrets em runtime eliminam uma categoria inteira de leaks. Digest imutável garante que produção é reproduzível.

Cada item tem trade-off em complexidade de setup. Mas o trade-off de não fazer é pago em incidentes, em cleanup pós-breach, em deploys que falham às 2 da manhã. Se a sua imagem de produção não passa em pelo menos metade dos itens acima, você está rodando dívida técnica que eventualmente vence.


Se sua infraestrutura depende de imagens Docker e você não tem certeza de quantas delas passariam em uma auditoria de segurança ou sobreviveriam a um restart às 3 da manhã, vale 30 minutos para mapear onde estão os riscos. A gente faz isso sem compromisso.

Fale com um especialista da Eficify

CompartilharSeguir
Bruno Carrilhos

SOBRE O AUTOR

Bruno Carrilhos

CTO · Eficify

Executivo de tecnologia, cofundador da Eficify, com mais de 20 anos de experiência na criação, evolução e sustentação de soluções digitais. Atua nas áreas de desenvolvimento de software, dados, inteligência artificial, cloud computing, cibersegurança e operações de missão crítica. É bacharel em Ciência da Computação, com formação em Ciência de Dados e Inteligência Artificial e pós-graduação em Segurança da Informação.

VAMOS CONVERSAR

Entregar em produção não precisa dar medo.

Conte seu cenário e receba um diagnóstico do seu fluxo de deploy e operação, sem compromisso.

Falar com um especialista