Voltar para Artigos
DevOps10 min de leitura

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.

12 de agosto de 2026

"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:

.dockerignore
node_modules
dist
.git
.gitignore
.env
.env.*
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
README.md
.DS_Store
coverage
.nyc_output

Dockerfile para Desenvolvimento

Dockerfile.dev
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:

Dockerfile
# ── 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 devDeps

docker-compose.yml para Desenvolvimento

docker-compose.yml
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: bridge

Comandos Essenciais

bash
# 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:latest

Use 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.