Docker em produção: por que uma imagem que funciona nem sempre é uma boa imagem
Criar uma imagem Docker não é apenas empacotar a aplicação e subir o container. Uma boa imagem precisa ser leve, segura, previsível e preparada para rodar bem em produção.
Docker em produção: por que uma imagem que funciona nem sempre é uma boa imagem
No mundo do Docker, existe uma diferença enorme entre uma imagem que “sobe” e uma imagem que realmente está pronta para produção.
Muita gente começa criando um Dockerfile com uma meta bem simples: fazer a aplicação rodar. E tudo bem, esse é o primeiro passo. O problema começa quando esse primeiro passo vira padrão definitivo.
Uma imagem Docker ruim pode funcionar perfeitamente hoje e, ainda assim, trazer problemas amanhã: builds lentos, deploys pesados, mais vulnerabilidades, consumo exagerado de recursos e dificuldade para investigar erros.
Docker não é só fazer a aplicação rodar. É fazer ela rodar bem.
O erro clássico: pensar só no “funciona”
Um erro comum é montar a imagem copiando tudo de uma vez, instalando dependências sem critério e usando imagens base genéricas, como node:latest.
A imagem até funciona. Mas geralmente vem com alguns problemas escondidos:
- tamanho muito maior do que o necessário;
- dependências de desenvolvimento dentro da imagem final;
- baixa previsibilidade entre builds;
- cache mal aproveitado;
- maior superfície de ataque;
- ausência de verificação de saúde do container.
É aquele famoso caso: “na minha máquina funciona”. Só que agora a máquina é um container de 1.4 GB rodando em produção. Aí já não é container, é mudança de apartamento.

O que uma boa imagem Docker precisa ter
Uma boa imagem Docker deve ser enxuta, previsível e segura.
Isso não significa criar um Dockerfile cheio de firula. Significa tomar decisões simples, mas importantes.
A principal mudança de mentalidade é separar o que é necessário para construir a aplicação do que é necessário para executar a aplicação.
Em outras palavras: o ambiente de build pode ter ferramentas, compiladores e dependências extras. Mas a imagem final precisa carregar apenas o essencial para rodar.
É aqui que entra o multi-stage build.
Multi-stage build: menos peso, menos problema
O multi-stage build permite criar uma etapa para compilar ou preparar a aplicação e outra etapa separada para executar o resultado final.
Em uma aplicação Node.js, por exemplo, a primeira etapa pode instalar dependências e gerar o build. A segunda etapa copia apenas o resultado final e instala somente o necessário para produção.
Um exemplo simplificado:
FROM node:18-alpine AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:18-alpine AS runtime
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
ENV NODE_ENV=production
CMD ["node", "dist/server.js"]
A diferença parece pequena, mas o impacto é grande.
A imagem final fica menor porque não carrega tudo que foi usado no build. Isso reduz espaço em disco, tempo de download, tempo de deploy e até possíveis brechas de segurança.
Por que usar npm ci em vez de npm install
No ambiente Node.js, outro detalhe importante é usar npm ci em builds automatizados.
O npm install pode atualizar o arquivo de lock ou resolver dependências de forma menos rígida. Já o npm ci usa exatamente o que está no package-lock.json.
Na prática, isso gera builds mais previsíveis.
E previsibilidade em produção é ouro. Ninguém quer descobrir que uma dependência mudou sozinha durante o deploy. Surpresa boa é pizza chegando, não pacote quebrando em produção.
Cache de camadas: build mais rápido sem mágica
O Docker trabalha com camadas. Cada instrução do Dockerfile gera uma camada que pode ser reaproveitada em builds futuros.
Por isso, uma boa prática é copiar primeiro os arquivos de dependência:
COPY package.json package-lock.json ./
RUN npm ci
Só depois disso o restante do código entra:
COPY . .
Assim, se você alterar apenas o código da aplicação, o Docker não precisa reinstalar todas as dependências de novo.
Esse pequeno ajuste pode reduzir bastante o tempo de build em pipelines de CI/CD.
Imagem final não precisa carregar dependência de desenvolvimento
Dependências de desenvolvimento são úteis para testar, compilar, formatar código e rodar ferramentas locais.
Mas em produção, elas geralmente não deveriam estar dentro da imagem final.
Por isso, comandos como este fazem diferença:
RUN npm ci --omit=dev
Isso mantém apenas as dependências necessárias para rodar a aplicação.
Menos dependências significa:
- imagem menor;
- menos pacotes vulneráveis;
- menor tempo de instalação;
- ambiente mais limpo;
- manutenção mais simples.
É o famoso “menos é mais”, só que sem virar frase de caneca corporativa.
NODE_ENV=production não é detalhe
Definir a variável:
ENV NODE_ENV=production
também é importante.
Muitas bibliotecas mudam comportamento com base nessa variável. Em modo produção, elas podem reduzir logs desnecessários, otimizar execução e evitar comportamentos voltados para desenvolvimento.
Parece um detalhe pequeno, mas é um daqueles detalhes que separam uma imagem improvisada de uma imagem bem preparada.
HEALTHCHECK: o container está vivo ou só fingindo?
Um container pode estar “rodando” e, mesmo assim, a aplicação dentro dele estar travada.
É por isso que o HEALTHCHECK é tão útil.
Exemplo:
HEALTHCHECK --interval=30s --timeout=5s \
CMD curl -f http://localhost:8080/health || exit 1
Com isso, o Docker consegue verificar se a aplicação realmente está respondendo.
Esse tipo de verificação ajuda em ambientes com orquestração, monitoramento e automação de deploy. Afinal, não basta o processo existir. Ele precisa estar saudável.
Benefícios práticos de uma imagem Docker bem feita
Uma imagem Docker bem construída melhora diretamente a operação da aplicação.
Os ganhos mais comuns são:
- deploys mais rápidos;
- menor consumo de banda e armazenamento;
- menos vulnerabilidades;
- builds mais previsíveis;
- melhor aproveitamento de cache;
- facilidade para escalar containers;
- ambiente de produção mais limpo.
No dia a dia, isso significa menos tempo esperando pipeline, menos alerta desnecessário e menos susto em produção.

Dockerfile bom é parte da estratégia de produção
É comum tratar o Dockerfile como um arquivo secundário. Algo que “só serve para subir a aplicação”.
Mas, na prática, ele faz parte da estratégia de entrega.
Um Dockerfile mal feito pode afetar segurança, custo, tempo de deploy e estabilidade. Já um Dockerfile bem escrito ajuda o time a entregar mais rápido e com menos risco.
Isso vale para projetos pequenos, startups, sistemas internos e aplicações grandes. Container ruim em escala pequena incomoda. Container ruim em escala grande vira boleto.
Conclusão
Uma imagem Docker boa não é aquela que apenas funciona. É aquela que funciona bem, de forma leve, segura, previsível e preparada para produção.
Usar multi-stage build, aproveitar o cache de camadas, instalar apenas dependências necessárias, definir NODE_ENV=production e incluir um HEALTHCHECK são práticas simples que fazem uma grande diferença.
No fim, Docker não deve ser tratado como um empacotador qualquer. Ele é parte da infraestrutura da aplicação.
E quando a infraestrutura é bem cuidada, o deploy fica mais rápido, a operação fica mais tranquila e o time dorme melhor.