Guides
Fundamentos ▾
Versionamento ▾
Deploy ▾

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.

  1. Push código para GitHub
  2. Acessar vercel.comAdd New Project
  3. Importar repositório → Vercel detecta framework
  4. 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

ComandoAção
pm2 listListar processos
pm2 logs meu-appLogs em tempo real
pm2 reload meu-appZero-downtime reload
pm2 restart meu-appReiniciar
pm2 stop meu-appParar
pm2 monitMonitor 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

SecretValor
SSH_HOSTIP ou domínio do servidor
SSH_USERusuário no servidor (ex: alfredo)
SSH_KEYconteú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