Otimizando seu Docker: Guia Prático com Node.js e Postgres
Se você está começando no mundo do Docker, provavelmente já se deparou com imagens que ficam gigantescas ou builds que demoram uma eternidade. A boa notícia é que, com algumas técnicas simples, podemos melhorar drasticamente o tamanho e a performance das nossas imagens.
Neste post, vamos criar um projeto do zero: uma aplicação básica em Express.js (Node.js) que se conecta a um banco de dados PostgreSQL. Faremos tudo isso rodar com Docker e, o mais importante, vamos aplicar otimizações essenciais, como imagens Alpine e multi-stage builds.
Vamos lá?
Pré-requisitos
Para seguir este tutorial, você só precisa ter o Docker e o Docker Compose instalados na sua máquina.
Passo 1: Criando a Aplicação Express Básica
Primeiro, vamos criar uma estrutura de pastas simples para nosso projeto.
mkdir meu-projeto-docker
cd meu-projeto-docker
Dentro dessa pasta, crie dois arquivos:
1. faça o npm init -y - Para inicializar um novo projeto Node.js e criar o package.json.
npm init -y
npm install express
crie um arquivo chamado app.js, docker-compose.yaml e outro chamado Dockerfile.
2. app.js - Nosso servidor web super simples.
const express = require('express');
const app = express();
const PORT = 3000;
app.get('/', (req, res) => {
res.send('Olá, mundo!');
});
app.listen(PORT, () => {
console.log(`Servidor rodando na porta ${PORT}`);
});
E é isso! Temos um app que simplesmente responde “Olá, mundo!” na rota principal.
Passo 2: O Dockerfile sem otimização
Antes de otimizar, vamos ver como um Dockerfile comum e não otimizado se parece. Isso nos dará uma base de comparação.
# 1. Usa uma imagem grande do Node.js
FROM node:22
# 2. Define o diretório de trabalho
WORKDIR /app
# 3. Copia TUDO para dentro do container
# Se qualquer arquivo mudar, o cache é invalidado aqui
COPY . .
# 4. Instala TODAS as dependências
RUN npm install
# 5. Expõe a porta e inicia o app
EXPOSE 3000
CMD ["node", "app.js"]
para fazer a build basta executar o comando:
docker build -t meu_app_express .
Problemas desta abordagem:
- Tamanho da Imagem: A imagem
node:22é baseada no Debian e inclui muitas ferramentas que não precisamos para rodar nossa aplicação, resultando em uma imagem final enorme (mais de 900MB!). - Cache Ineficiente: A linha
COPY . .copia todos os arquivos do projeto de uma vez. Se você alterar apenas um caractere no seuapp.js, o Docker invalidará o cache desta camada e de todas as subsequentes, forçando onpm installa rodar novamente, mesmo que as dependências não tenham mudado.
no total a imagem ficou com
meuapp latest 7350c614c371 3 minutes ago 1.13GB
Passo 3: O Dockerfile Otimizado
Agora, vamos criar o nosso Dockerfile de verdade, usando duas técnicas poderosas:
- Imagens Alpine: São versões super enxutas de sistemas operacionais. A
node:22-alpineé muito menor que anode:22. - Multi-stage Builds: Permite que usemos um ambiente para “construir” nossa aplicação (instalando dependências) e outro ambiente, limpo e enxuto, para “rodar” a aplicação. Apenas os artefatos necessários são copiados para o estágio final.
Crie o arquivo Dockerfile na raiz do seu projeto com o seguinte conteúdo:
# Dockerfile (Otimizado)
# ---- Estágio 1: Build ----
# Usamos a imagem alpine do Node como um "builder" (construtor)
FROM node:22-alpine AS builder
# Define o diretório de trabalho
WORKDIR /app
# Copia APENAS o package.json e package-lock.json (se existir)
# Isso aproveita o cache do Docker. O npm install só rodará de novo se estes arquivos mudarem.
COPY package*.json ./
# Instala apenas as dependências de produção
RUN npm install --production
# ---- Estágio 2: Produção ----
# Começamos de uma nova imagem limpa e leve
FROM node:22-alpine
WORKDIR /app
# Copia as dependências já instaladas do estágio "builder"
COPY --from=builder /app/node_modules ./node_modules
# Copia o código da nossa aplicação
COPY app.js ./
# Expõe a porta e define o comando para rodar o app
EXPOSE 3000
CMD ["node", "app.js"]
O que ganhamos com isso?
- Imagens Menores: A imagem final terá um tamanho drasticamente reduzido (geralmente em torno de 150MB), pois é baseada na
alpinee contém apenas o necessário para rodar o app. - Builds mais Rápidos: Como copiamos o
package.jsonprimeiro, a demorada etapanpm installsó será executada novamente se as dependências mudarem, não a cada alteração no código.
com essas otimzaçoes a imagem final ficou com:
meuapp latest 3c8f1b2d4e5a 2 minutes ago 162MB
Passo 4: Orquestrando Tudo com o Docker Compose
Agora precisamos de um banco de dados PostgreSQL e queremos que ele “converse” com nossa aplicação. O Docker Compose é perfeito para gerenciar múltiplos contêineres.
Crie o arquivo docker-compose.yml na raiz do projeto:
# docker-compose.yml
services:
# Nosso serviço da aplicação Express
app:
build: . # Constrói a imagem usando o Dockerfile na pasta atual
container_name: meu_app_express
ports:
- "3000:3000" # Mapeia a porta 3000 do host para a 3000 do contêiner
depends_on:
- db # Diz ao Docker para iniciar o serviço 'db' antes do 'app'
# Nosso serviço do banco de dados PostgreSQL
db:
image: postgres:15-alpine # Usando a imagem alpine do Postgres também!
container_name: meu_banco_postgres
environment:
POSTGRES_USER: admin
POSTGRES_PASSWORD: mysecretpassword
POSTGRES_DB: mydatabase
ports:
- "5432:5432" # Mapeia a porta do Postgres (opcional, bom para debug)
volumes:
- postgres_data:/var/lib/postgresql/data # Garante que os dados persistam
# Define um volume nomeado para persistir os dados do banco
volumes:
postgres_data:
O que este arquivo faz?
services: Define os contêineres que farão parte da nossa aplicação.app: Nosso app Node.js. A instruçãobuild: .diz ao Compose para construir a imagem a partir do nossoDockerfileotimizado.db: Nosso banco de dados. Usamos uma imagem prontapostgres:15-alpinepara manter a otimização.
environment: Configura as variáveis de ambiente para criar o usuário e o banco de dados no Postgres na primeira vez que ele iniciar.volumes: Esta é uma parte crucial! A linhapostgres_data:/var/lib/postgresql/datacria um “volume”. Isso significa que os dados do seu banco serão salvos fora do contêiner, em uma área gerenciada pelo Docker. Se você parar e remover o contêiner, seus dados não serão perdidos!
Passo 5: Rodando o Projeto
Sua estrutura de arquivos final deve ser:
meu-projeto-docker/
├── app.js
├── Dockerfile
├── docker-compose.yml
└── package.json
Agora, no seu terminal, dentro da pasta meu-projeto-docker, execute o comando:
docker-compose up --build
up: Inicia os contêineres definidos nodocker-compose.yml.--build: Força o Docker a construir a imagem do nossoappantes de iniciá-la.
Você verá os logs de ambos os contêineres no seu terminal. Espere até ver a mensagem Servidor rodando na porta 3000.
Teste sua aplicação! Abra seu navegador e acesse http://localhost:3000. Você deverá ver a mensagem:
Olá, mundo! Meu app Express otimizado está no ar! 🚀
Para parar e remover todos os contêineres, redes e volumes criados, pressione Ctrl + C no terminal e depois execute:
docker-compose down
Conclusão
Parabéns! Você não apenas criou uma aplicação com Node.js e Postgres rodando em Docker, mas também aprendeu duas das otimizações mais importantes para o dia a dia:
- Usar imagens
-alpinepara reduzir drasticamente o tamanho final. - Implementar
multi-stage buildspara criar imagens menores e acelerar o processo de build.
Essas práticas simples tornam seus projetos mais eficientes, fáceis de gerenciar e mais rápidos de implantar. A partir daqui, você pode evoluir o projeto para realmente conectar o Express ao Postgres, mas a base otimizada já está pronta!