Deploy de Apps React/Next.js
5 estratégias: Vercel, SPA+Nginx, SPA+Docker, SSR+PM2, SSR+Docker. CI/CD com GitHub Actions.
Mapa de Decisão
Meu app é...
├── SPA (Vite / CRA / React Router)
│ ├── Simplicidade máxima → Vercel
│ └── Controle total / VPS
│ ├── Sem Docker → Nginx nativo
│ └── Com Docker → multi-stage (builder+nginx)
│
└── SSR / Next.js
├── Simplicidade máxima → Vercel (criador do Next.js)
└── VPS
├── Sem Docker → PM2 + Nginx proxy reverso
└── Com Docker → multi-stage com Node runtime
Opção A
Vercel
SPA SSR Grátis
- Zero config
- CI/CD automático
- Edge network global
Opção B
SPA + Nginx
SPA
- Simples no VPS
- Sem container overhead
- Deploy manual
Opção C
SPA + Docker
SPA Docker
- Portável
- Rollback por tag de imagem
Opção D
SSR + PM2 + Nginx
SSR
- Simples
- Zero-downtime reload
- Sem isolamento
Opção E
SSR + Docker
SSR Docker
- Isolamento total
- Imagem reproduzível
VPS — Fundamentos
# Node via NVM
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
source ~/.bashrc
nvm install --lts && nvm use --lts
# Nginx
sudo apt install nginx -y
sudo ufw allow 'Nginx Full'
A. Vercel
Mais fácil. CI/CD automático a cada push no main.
- Push código para GitHub
- Acessar
vercel.com→ Add New Project - Importar repositório → Vercel detecta framework
- Deploy — URL gerada em ~1 min
Variáveis de ambiente:
Settings → Environment Variables. Qualquer push para main → rebuild automático.B. SPA no VPS — Nginx Nativo
cd ~/apps/meu-app
git pull origin main
npm ci
npm run build
sudo mkdir -p /var/www/meu-app
sudo cp -r dist/* /var/www/meu-app/
sudo chown -R www-data:www-data /var/www/meu-app
/etc/nginx/sites-available/meu-app
server {
listen 80;
server_name meu-app.com;
root /var/www/meu-app;
index index.html;
# SPA routing — todas as rotas → index.html
location / {
try_files $uri $uri/ /index.html;
}
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
}
sudo ln -s /etc/nginx/sites-available/meu-app /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
C. SPA no VPS — Docker Multi-Stage
Stage 1
builder
node:20-alpine
- npm ci
- npm run build
- → /app/dist
→
Stage 2
runner
nginx:alpine
- COPY dist/
- nginx.conf
- EXPOSE 80
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM nginx:alpine AS runner
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
nginx.conf para SPA
server {
listen 80;
root /usr/share/nginx/html;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
}
docker build -t meu-app:latest .
docker run -d --name meu-app -p 3000:80 --restart unless-stopped meu-app:latest
D. SSR/Next.js — PM2 + Nginx
npm install -g pm2
cd ~/apps/meu-next-app
git pull origin main
npm ci && npm run build
pm2 start npm --name "meu-app" -- start
pm2 save
pm2 startup # copiar e rodar o comando gerado
Nginx — proxy reverso
server {
listen 80;
server_name meu-app.com;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
}
}
PM2 — Comandos
| Comando | Ação |
|---|---|
| pm2 list | Listar processos |
| pm2 logs meu-app | Logs em tempo real |
| pm2 reload meu-app | Zero-downtime reload |
| pm2 restart meu-app | Reiniciar |
| pm2 stop meu-app | Parar |
| pm2 monit | Monitor interativo |
E. SSR/Next.js — Docker (4 stages)
Stage 1
base
node:20-alpine
- WORKDIR /app
- package.json
→
Stage 2
deps
← base
- npm ci
→
Stage 3
builder
← deps
- COPY . .
- npm run build
→
Stage 4
runner
node:20-alpine
- standalone
- node server.js
FROM node:20-alpine AS base
WORKDIR /app
COPY package*.json ./
FROM base AS dependencies
RUN npm ci
FROM dependencies AS builder
COPY . .
RUN npm run build
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
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"]
next.config.js precisa de output: 'standalone' para o stage runner funcionar.docker build -t meu-next-app:latest .
docker run -d --name meu-next-app -p 3000:3000 \
--restart unless-stopped \
-e NODE_ENV=production \
meu-next-app:latest
CI/CD com GitHub Actions
git push
main branch
→
Actions
workflow dispara
→
SSH
appleboy/ssh-action
→
Servidor
git pull + build
→
Deploy
pm2 reload
Setup de Secrets
Gerar chave SSH dedicada para CI/CD no servidor:
ssh-keygen -t ed25519 -C "github-actions" -f ~/.ssh/github_actions -N ""
cat ~/.ssh/github_actions.pub >> ~/.ssh/authorized_keys
cat ~/.ssh/github_actions # copiar este conteúdo
No repositório GitHub: Settings → Secrets and variables → Actions → New repository secret
| Secret | Valor |
|---|---|
| SSH_HOST | IP ou domínio do servidor |
| SSH_USER | usuário no servidor (ex: alfredo) |
| SSH_KEY | conteúdo de ~/.ssh/github_actions (privado) |
Workflow YAML
.github/workflows/deploy.yml
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Deploy via SSH
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ secrets.SSH_HOST }}
username: ${{ secrets.SSH_USER }}
key: ${{ secrets.SSH_KEY }}
script: |
cd ~/apps/meu-app
git pull origin main
npm ci
npm run build
pm2 reload meu-app
Variação — Docker
script: |
cd ~/apps/meu-app
git pull origin main
docker build -t meu-app:latest .
docker stop meu-app || true
docker rm meu-app || true
docker run -d --name meu-app -p 3000:3000 \
--restart unless-stopped meu-app:latest
Variação — SPA estático
script: |
cd ~/apps/meu-app
git pull origin main
npm ci && npm run build
sudo cp -r dist/* /var/www/meu-app/
Comparação Geral
| Estratégia | SPA | SSR | Custo | Config | Rollback | Isolamento |
|---|---|---|---|---|---|---|
| Vercel | ✅ | ✅ | grátis/pago | mínima | automático | total |
| Nginx Nativo | ✅ | ❌ | VPS | média | manual | nenhum |
| SPA + Docker | ✅ | ❌ | VPS | média | tag de imagem | container |
| PM2 + Nginx | ❌ | ✅ | VPS | média | manual | nenhum |
| SSR + Docker | ~ | ✅ | VPS | alta | tag de imagem | container |
Variáveis de Ambiente
# .env.production no servidor (nunca comitar)
cp .env.example .env.production
nvim .env.production
# PM2 ecosystem (ecosystem.config.js)
module.exports = {
apps: [{
name: 'meu-app',
script: 'node_modules/.bin/next',
args: 'start',
env_production: {
NODE_ENV: 'production',
PORT: 3000
}
}]
}
# Docker
docker run -d --env-file .env.production meu-app:latest