Se você é dev frontend e nunca usou Docker, provavelmente já ouviu que "funciona na minha máquina". Docker resolve exatamente isso — ele empacota sua aplicação com todas as dependências em um container que roda igual em qualquer lugar: no seu Mac, no PC do colega, no CI e em produção. Neste guia, vou do zero ao deploy com foco em projetos frontend reais.
O que é Docker (sem enrolação)
Docker é uma ferramenta que cria ambientes isolados (containers) para rodar aplicações. Diferente de uma máquina virtual, um container é leve — compartilha o kernel do sistema e inicia em segundos.
- Imagem — o "molde". Contém o sistema operacional base, Node.js, suas dependências e seu código.
- Container — a "instância" rodando da imagem. Você pode ter vários containers da mesma imagem.
- Dockerfile — a "receita" que define como construir a imagem.
- docker-compose — orquestra múltiplos containers (app + banco + redis) em um só comando.
Instalação
Instale o Docker Desktop para seu sistema operacional. Ele inclui Docker Engine, Docker CLI e Docker Compose:
- macOS: baixe em docker.com/products/docker-desktop ou use
brew install --cask docker - Windows: baixe o Docker Desktop (requer WSL2)
- Linux: instale via apt/yum ou siga a documentação oficial
Verifique a instalação:
docker --version
# Docker version 24.x.x
docker compose version
# Docker Compose version v2.x.xPrimeiro Dockerfile: aplicação Next.js
Vamos criar um Dockerfile para uma aplicação Next.js. O mais básico possível para entender o conceito:
# Dockerfile
FROM node:20-alpine
WORKDIR /app
# Copiar dependências primeiro (cache de layers)
COPY package.json pnpm-lock.yaml ./
RUN corepack enable && pnpm install --frozen-lockfile
# Copiar código
COPY . .
# Build
RUN pnpm build
# Rodar
EXPOSE 3000
CMD ["pnpm", "start"]Cada linha é uma layer. O Docker faz cache de cada layer — se o package.json não mudou, ele pula a instalação de dependências. Por isso copiamos as dependências antes do código.
# Construir a imagem
docker build -t meu-app .
# Rodar o container
docker run -p 3000:3000 meu-app
# Acessar: http://localhost:3000.dockerignore: o que não entra na imagem
Assim como o .gitignore, o .dockerignore define o que não deve ser copiado para dentro da imagem. Isso reduz o tamanho e acelera o build:
# .dockerignore
node_modules
.next
.git
.env
.env.local
README.md
.vscode
.claudeMulti-stage build: imagem de produção otimizada
O Dockerfile básico funciona, mas a imagem final é pesada — contém todas as devDependencies, código fonte e arquivos de build. Em produção, você só precisa do output do build. Multi-stage build resolve isso:
# Dockerfile (multi-stage)
# Stage 1: Instalar dependências
FROM node:20-alpine AS deps
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN corepack enable && pnpm install --frozen-lockfile
# Stage 2: Build
FROM node:20-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN corepack enable && pnpm build
# Stage 3: Produção (só o necessário)
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
# Copiar apenas o necessário do build
COPY --from=builder /app/public ./public
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
EXPOSE 3000
CMD ["node", "server.js"]A diferença é significativa:
- Dockerfile básico: ~1GB (node_modules completo, código fonte, devDependencies)
- Multi-stage: ~150MB (só Node.js + output do build + arquivos estáticos)
Docker Compose: ambiente de dev completo
Para desenvolvimento local, docker-compose orquestra múltiplos serviços. Um cenário real: app Next.js + PostgreSQL + Redis:
# docker-compose.yml
version: '3.8'
services:
app:
build: .
ports:
- '3000:3000'
volumes:
- .:/app
- /app/node_modules
environment:
- DATABASE_URL=postgresql://user:pass@db:5432/myapp
- REDIS_URL=redis://cache:6379
depends_on:
- db
- cache
db:
image: postgres:16-alpine
ports:
- '5432:5432'
environment:
POSTGRES_USER: user
POSTGRES_PASSWORD: pass
POSTGRES_DB: myapp
volumes:
- pgdata:/var/lib/postgresql/data
cache:
image: redis:7-alpine
ports:
- '6379:6379'
volumes:
pgdata:# Subir tudo com um comando
docker compose up -d
# Ver logs
docker compose logs -f app
# Derrubar tudo
docker compose down
# Derrubar e limpar volumes (reset do banco)
docker compose down -vO volumes: - .:/app monta seu código local dentro do container — mudanças no código refletem imediatamente sem rebuild. O - /app/node_modules evita que o node_modules local sobrescreva o do container.
Ambiente de dev vs produção
É comum ter dois Dockerfiles ou usar target no compose:
# docker-compose.dev.yml
services:
app:
build:
context: .
dockerfile: Dockerfile.dev
ports:
- '3000:3000'
volumes:
- .:/app
- /app/node_modules
command: pnpm dev# Dockerfile.dev (simples, sem multi-stage)
FROM node:20-alpine
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN corepack enable && pnpm install
COPY . .
EXPOSE 3000
CMD ["pnpm", "dev"]# Dev
docker compose -f docker-compose.dev.yml up
# Produção
docker compose up --buildVariáveis de ambiente
Nunca coloque secrets no Dockerfile. Use variáveis de ambiente via .env ou docker-compose:
# .env (não commitar)
DATABASE_URL=postgresql://user:pass@db:5432/myapp
STRIPE_SECRET_KEY=sk_test_...
NEXT_PUBLIC_API_URL=http://localhost:3000/api# docker-compose.yml
services:
app:
env_file:
- .env
# ou individualmente:
environment:
- NODE_ENV=productionComandos essenciais
Os comandos que uso diariamente:
# Imagens
docker build -t app . # Construir imagem
docker images # Listar imagens
docker rmi app # Remover imagem
# Containers
docker ps # Containers rodando
docker ps -a # Todos (incluindo parados)
docker logs -f <container> # Logs em tempo real
docker exec -it <container> sh # Entrar no container
docker stop <container> # Parar
docker rm <container> # Remover
# Compose
docker compose up -d # Subir em background
docker compose down # Derrubar
docker compose down -v # Derrubar + limpar volumes
docker compose logs -f # Logs de todos os serviços
docker compose build --no-cache # Rebuild sem cache
# Limpeza
docker system prune -a # Remover tudo que não está em usoDeploy em produção
Com a imagem multi-stage pronta, o deploy é enviar para um registry e rodar no servidor:
# Build e tag
docker build -t meu-app:latest .
# Push para Docker Hub (ou ECR, GCR, etc.)
docker tag meu-app:latest usuario/meu-app:latest
docker push usuario/meu-app:latest
# No servidor (pull e run)
docker pull usuario/meu-app:latest
docker run -d -p 3000:3000 --env-file .env usuario/meu-app:latestServiços como Render, Railway e Fly.io detectam o Dockerfile automaticamente e fazem build + deploy sem configuração extra.
Erros comuns
- node_modules dentro da imagem + volume local — o volume sobrescreve o node_modules do container. Solução: adicionar
- /app/node_modulesno volumes. - Imagem pesada demais — usar
node:20ao invés denode:20-alpine. Alpine é ~5x menor. - Build lento — não está aproveitando cache de layers. Copie
package.jsonantes do código. - Porta não acessível — esqueceu o
EXPOSEou o mapeamento de porta nodocker run -p. - Variáveis de ambiente não chegam — em Next.js, variáveis
NEXT_PUBLIC_*precisam estar disponíveis no build time, não só no runtime.
Resumo
Docker não é só para DevOps — é uma ferramenta de produtividade para qualquer dev. Com um Dockerfile e um docker-compose, você garante que o ambiente é consistente do dev ao deploy. O investimento de aprendizado é pequeno comparado ao retorno: menos bugs de ambiente, onboarding mais rápido, e deploy previsível.
