Vinicius Aguiar
DevOps

Docker para devs frontend: do zero ao deploy

16 de abril de 2026 · 10 min de leitura

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

Primeiro 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
.claude

Multi-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 -v

O 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 --build

Variá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=production

Comandos 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 uso

Deploy 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:latest

Serviç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_modules no volumes.
  • Imagem pesada demais — usar node:20 ao invés de node:20-alpine. Alpine é ~5x menor.
  • Build lento — não está aproveitando cache de layers. Copie package.json antes do código.
  • Porta não acessível — esqueceu o EXPOSE ou o mapeamento de porta no docker 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.