Containerizando sua API Node.js e Redis com Docker
Dockerfile multi-stage para imagens enxutas em produção, .dockerignore, docker-compose.yml com healthchecks, volumes nomeados, redes e variáveis de ambiente. Do dev ao build de produção.
"Na minha máquina funciona" é uma das frases mais caras do desenvolvimento de software. O desenvolvedor usa Node.js 20, o servidor de produção tem Node.js 18. Localmente o Redis está na versão 6, em produção está na 7 com comportamentos diferentes. O banco está em MySQL localmente, em PostgreSQL em produção.
Docker encapsula a aplicação e todas as suas dependências em um container — um ambiente isolado que roda idêntico em qualquer máquina. Este artigo cobre do Dockerfile básico ao multi-stage build para produção, com docker-compose.yml completo com healthchecks, volumes nomeados e configuração de rede.
Conceitos Fundamentais
- Image — snapshot imutável do sistema (Node.js + código + dependências). Criada pelo Dockerfile.
- Container — instância em execução de uma image. Efêmero por padrão (dados perdidos ao parar).
- Volume — storage persistente fora do container (dados do banco, uploads).
- Network — rede virtual que conecta containers. Containers na mesma network se comunicam pelo nome do serviço.
- Docker Compose — orquestrador de múltiplos containers definido em YAML.
.dockerignore: Antes do Dockerfile
O multi-stage build é a técnica mais importante para imagens de produção: o primeiro estágio (builder) instala todas as dependências e compila o TypeScript. O segundo estágio (production) copia apenas o dist/ compilado e as node_modules de produção — sem o compilador TypeScript, sem as devDependencies, sem o código-fonte. O resultado típico é uma imagem que vai de ~800MB para ~150MB. O .dockerignore garante que o node_modules local não seja copiado para o contexto do build (o que tornaria o build extremamente lento).
Sempre crie o .dockerignore antes do Dockerfile. Ele impede que arquivos desnecessários (node_modules, .git, .env) sejam copiados para a image, reduzindo tamanho e tempo de build:
node_modules
dist
.git
.gitignore
.env
.env.*
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
README.md
.DS_Store
coverage
.nyc_outputDockerfile para Desenvolvimento
FROM node:22-alpine
# Cria usuário não-root (segurança — nunca rode como root em produção)
RUN addgroup -g 1001 nodejs && adduser -u 1001 -G nodejs -s /bin/sh -D appuser
WORKDIR /app
# Copia apenas package files primeiro (Docker layer cache)
# Se estes arquivos não mudarem, 'npm install' não roda de novo no próximo build
COPY package*.json ./
# npm ci: instalação determinística baseada no package-lock.json
RUN npm ci
# Copia o restante do código
COPY --chown=appuser:nodejs . .
USER appuser
EXPOSE 3333
# Modo dev: usa ts-node-dev para hot reload
CMD ["npm", "run", "dev"]Dockerfile Multi-Stage para Produção
O multi-stage build usa múltiplos FROM no mesmo Dockerfile. O stage de build tem todas as devDependencies e ferramentas de compilação. O stage final (produção) copia apenas o output compilado — a image resultante é muito menor:
# ── Stage 1: Dependências ─────────────────────────────────────────────────────
FROM node:22-alpine AS deps
WORKDIR /app
COPY package*.json ./
# npm ci --only=production: instala apenas dependências de produção
RUN npm ci --only=production
# ── Stage 2: Build ────────────────────────────────────────────────────────────
FROM node:22-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci # Instala tudo (incluindo devDeps para compilar)
COPY . .
RUN npm run build # Compila TypeScript → dist/
# ── Stage 3: Produção ─────────────────────────────────────────────────────────
FROM node:22-alpine AS production
# Usuário não-root
RUN addgroup -g 1001 nodejs && adduser -u 1001 -G nodejs -s /bin/sh -D appuser
WORKDIR /app
# Copia apenas o necessário dos stages anteriores
COPY --from=deps --chown=appuser:nodejs /app/node_modules ./node_modules
COPY --from=builder --chown=appuser:nodejs /app/dist ./dist
COPY --from=builder --chown=appuser:nodejs /app/package.json ./
# Variável de ambiente para produção
ENV NODE_ENV=production
# Health check: Docker verifica se o container está saudável
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD wget -qO- http://localhost:3333/health || exit 1
USER appuser
EXPOSE 3333
CMD ["node", "dist/server.js"]
# Resultado: image de ~150MB ao invés de ~1GB com devDepsdocker-compose.yml para Desenvolvimento
name: minha-api
services:
app:
build:
context: .
dockerfile: Dockerfile.dev
container_name: api
# Espelha o código local para o container (hot reload)
volumes:
- .:/app
- /app/node_modules # Preserva o node_modules do container
ports:
- "3333:3333"
environment:
- NODE_ENV=development
- DATABASE_URL=postgresql://postgres:postgres@postgres:5432/dev
- REDIS_URL=redis://redis:6379
# Reinicia apenas se o container parar por erro (não se você parar manualmente)
restart: on-failure
depends_on:
postgres:
condition: service_healthy # Aguarda o healthcheck do PostgreSQL
redis:
condition: service_healthy
networks:
- app-network
postgres:
image: postgres:16-alpine
container_name: postgres
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: dev
ports:
- "5432:5432"
# Volume nomeado: dados persistem entre docker compose down/up
volumes:
- postgres_data:/var/lib/postgresql/data
# Healthcheck: reporta 'healthy' apenas quando o PostgreSQL aceitar conexões
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5
networks:
- app-network
redis:
image: redis:7-alpine
container_name: redis
ports:
- "6379:6379"
volumes:
- redis_data:/data
# Persistência RDB: salva snapshot a cada 60s se houver 1+ mudança
command: redis-server --save 60 1 --loglevel warning
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
networks:
- app-network
# Volumes nomeados: dados persistem fora do ciclo de vida dos containers
volumes:
postgres_data:
redis_data:
# Rede interna: os containers se comunicam por nome de serviço (ex: 'redis', 'postgres')
networks:
app-network:
driver: bridgeComandos Essenciais
# Sobe todos os serviços em background
docker compose up -d
# Reconstrói a image (quando Dockerfile ou dependências mudam)
docker compose up -d --build
# Ver logs de um serviço específico em tempo real
docker compose logs -f app
# Executa um comando dentro do container
docker compose exec app npm run migration:run
docker compose exec postgres psql -U postgres -d dev
# Para e remove containers (dados nos volumes persistem)
docker compose down
# Para e remove containers E volumes (limpa tudo)
docker compose down -v
# Status dos containers com healthcheck
docker compose ps
# Build da image de produção
docker build --target production -t minha-api:latest .
# Verifica o tamanho das layers
docker history minha-api:latestUse condition: service_healthy ao invés de apenas depends_on para garantir que o PostgreSQL e o Redis estejam realmente prontos antes de iniciar a API — não apenas que o container subiu. Sem isso, a API pode tentar conectar antes do banco estar aceitando conexões, causando erros de inicialização.
Conclusão
Um ambiente Docker bem configurado elimina o problema de "na minha máquina funciona", acelera o onboarding de novos desenvolvedores (um único docker compose up -d e o ambiente está pronto) e garante paridade entre desenvolvimento e produção. O multi-stage build para produção é essencial: uma image de 150MB ao invés de 1GB significa menos tempo de download em deploys, menor superfície de ataque e menor custo de armazenamento no registry.