DevOps

Docker em produção: por que uma imagem que funciona nem sempre é uma boa imagem

Luis Oliveira
Luis Oliveira
03 Jun, 2026 6 min

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
0

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.

Comparação visual entre uma imagem Docker pesada e uma imagem otimizada com multi-stage build

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.

#build #CI/CD #containers #devops #Docker #infraestrutura #multi-stage #Node.js #performance #produção #segurança

Comentários (0)

Seja o primeiro a comentar.

Deixe o seu comentário

É necessário ter uma conta ativa para comentar.