1
Deploy V.1.0.2
VPS 4 Deploy Agent edited this page 2026-05-30 16:37:14 +02:00

🚀 Deploy V.1.0.2 — GitOps + Modelo A otimizado para Wasabi

Versão 1.0.2 — RX agora usa Modelo A (build na VPS4, imagem pronta para VPS1) com Wasabi como storage. Tempo de deploy: ~30-40 segundos (era 3-7min com Modelo B). Evita os erros comuns documentados aqui.


📋 Índice

  1. Arquitetura Multi-VPS
  2. Pipeline GitOps
  3. Por que RX usa Modelo A (não B)
  4. O Listener do Webhook
  5. Versionamento do Servidor
  6. Versionamento do Cliente Windows
  7. Erros comuns e soluções
  8. Troubleshooting
  9. Changelog

1. Arquitetura Multi-VPS

Servidor IP (VPN) Função
VPS 1 — App 10.99.0.1 Produção (Docker rootless)
VPS 3 — Data/Git 10.99.0.3 Gitea + PostgreSQL
VPS 4 — Builder 10.99.0.4 Build + deploy
Wasabi S3-compatível Storage de imagens (não no disco)

2. Pipeline GitOps (Modelo A)

git push origin main
    ↓
Gitea dispara webhook → VPS4:9003/webhook
    ↓
listener-rx.js: valida token, dispara deploy-prod.sh
    ↓
VPS4: docker compose build app (cria imagem)
    ↓
VPS4: docker save → app.tar.gz (exporta imagem compilada)
    ↓
VPS4: rsync app.tar.gz + docker-compose.yml → VPS1
    ↓
VPS1: docker load < app.tar.gz (carrega imagem)
    ↓
VPS1: docker compose up -d --no-deps app (reinicia container)
    ↓
VPS4: git push chore(deploy): bump version (anti-loop)
    ↓
✅ Deploy concluído (~30-40 segundos)

Por que é rápido:

  • VPS4 faz o build pesado (docker compose build)
  • VPS1 só recebe a imagem pronta (docker load)
  • Nenhuma compilação na produção
  • Usa cache do Docker na VPS4 (npm install reutilizado)

3. Por que RX usa Modelo A (não B)

Modelo B (build na VPS1, descontinuado para RX):

VPS4: rsync código-fonte → VPS1
VPS1: docker compose build app  ← LENTO (3-7 min)

Problema: COPY . . no Dockerfile invalida cache a cada mudança de código. Docker tem que recompile tudo mesmo que package.json não tenha mudado.

RX com Wasabi:

  • Imagens estão no Wasabi (não no container)
  • Container é stateless (sem volumes de dados)
  • Modelo A é perfeito: build rápido na VPS4, imagem pronta na VPS1

Clube67 (modelo A histórico):

VPS4: npm install + npm build (front) ✓ rápido
VPS4: rsync dist/ → VPS1 ✓ rápido
VPS1: docker compose up (sem build) ✓ instantâneo

4. O Listener do Webhook

Arquivo: /home/deploy/stack/webhook-listener/listener-rx.js (porta 9003)

Responsabilidades:

  • Valida token (x-gitea-token ou ?token=...)
  • Trava de concorrência: isBuilding flag (não permite deploys paralelos)
  • Anti-loop: ignora commits chore(deploy): bump version (senão loop infinito)
  • Dispara deploy-prod.sh em background
  • Reporta status ao Gitea (pending → success/failure)

Configuração:

# /etc/systemd/system/webhook-rx.service
PORT=9003
WEBHOOK_TOKEN=rx_deploy_token_9003_c7f2a1b4e8d5
DEPLOY_SCRIPT=/home/deploy/stack/rx.scoreodonto.com/deploy-prod.sh
GITEA_TOKEN=<token da API do Gitea>

5. Versionamento — Servidor

Fonte de verdade: dental-server/version.txt

# Arquivo:
cat /home/deploy/stack/rx.scoreodonto.com/dental-server/version.txt
# Output: 2.1.4

# Ou via HTTP (injetado pelo server no sidebar):
curl -s http://rx.scoreodonto.com/ | grep -o 'v[0-9]\.[0-9]\.[0-9]'
# Output: v2.1.4

Como o versionamento funciona

Arquivo Quando muda
version.txt A cada deploy (auto-bump PATCH)
package.json NUNCA (preserva Docker cache)
index.html A cada deploy (versão injetada)
Server em runtime version.txt (não package.json)

Importante: update-version.js NÃO toca package.json:

// ❌ NÃO faz isso (invalida cache Docker):
fs.writeFileSync(packageFile, JSON.stringify(pkg, null, 2));

// ✅ Só toca esses:
fs.writeFileSync(versionFile, next);           // version.txt
fs.writeFileSync(indexFile, html);             // index.html (versão injetada)

6. Versionamento — Cliente Windows

Fonte de verdade: package.jsonlatest.yml

# Arquivo:
cat /home/deploy/stack/rx.scoreodonto.com-client/package.json | grep version
# Output: "version": "1.2.7"

# Gerado no build:
cat /home/deploy/stack/rx.scoreodonto.com/dental-server/updates/latest.yml
# Output:
# version: 1.2.7
# files:
#   - url: ScoreOdonto Dental Client Setup 1.2.7.exe

Fluxo de atualização nas clínicas:

Clínica inicia app
  ↓
electron-updater: GET /updates/latest.yml
  ↓
version no yml > versão instalada?
  ↓ SIM
  Baixa .exe + instala ao reiniciar
  ↓ NÃO
  Nada acontece

⚠️ CRÍTICO: Se latest.yml não for atualizado, clínicas não recebem update.


7. Erros comuns e soluções

Erro 1: previewStyle is not defined

Causa: Variável não definida no patientCardHTML() Solução: Sempre ter <img> ou placeholder, nunca deixar vazio

// ❌ ERRADO:
const preview = thumb ? `<img...>` : '';
<div class="image-preview"${previewStyle}>  // previewStyle não existe

// ✅ CORRETO:
const preview = thumb 
  ? `<img class="preview-img" ...>`
  : `<div class="preview-img-placeholder"></div>`;
<div class="image-preview">
  ${preview}
</div>

Erro 2: Deploy leva 3-7 minutos

Causa: Modelo B com docker compose build na VPS1 Solução: Usar Modelo A — build na VPS4, imagem para VPS1

# ❌ LENTO (Modelo B):
ssh VPS1 "docker compose build app && docker compose up"

# ✅ RÁPIDO (Modelo A):
docker compose build app  # VPS4 (local, cache reutilizado)
docker save | gzip > app.tar.gz
rsync app.tar.gz → VPS1
ssh VPS1 "docker load < app.tar.gz && docker compose up"

Erro 3: Docker cache sempre invalida

Causa: COPY . . no Dockerfile invalida cache quando código muda Solução: Não tocar package.json no version bump

# ❌ PROBLEMA:
FROM node:22-alpine
RUN npm install  # camada 1
COPY . .         # camada 2 (invalida sempre que código muda)

# ✅ SOLUÇÃO:
FROM node:22-alpine
COPY package*.json ./  # copia ANTES de install
RUN npm install        # camada 2 (reutiliza cache se package.json não muda)
COPY . .               # camada 3 (invalida, mas npm install já rodou)

Erro 4: Imagens dos cards ficam em branco

Causa: Sem thumb_filename no banco, sem placeholder no frontend Solução: SEMPRE gerar thumbnail + mostrar placeholder

// Servidor gera thumb ao receber imagem:
const thumbBuffer = await imageProcessor.getThumbnail(imageBuffer, 500, 500);
thumbFilename = `thumb_${filename}`;
await storage.saveImage(thumbFilename, thumbBuffer, ...);
// Salva no banco com thumb_filename

// Frontend SEMPRE mostra algo:
const preview = thumb
  ? `<img src="${thumb}" loading="lazy">`  // mostra thumb rápido
  : `<div class="placeholder"></div>`;     // fallback cinza enquanto carrega

Erro 5: Loop infinito de CI

Causa: Listener não ignora commits de bump de versão Solução: Regex anti-loop no listener

const VERSION_BUMP_RE = /^chore\(deploy\):\s*bump version/i;

if (messages.every(m => VERSION_BUMP_RE.test(m))) {
  // Ignora commit de bump → sem loop
  return res.json({ status: 'skipped' });
}

8. Troubleshooting

Problema Diagnóstico Solução
Deploy leva 3+ min Está usando Modelo B? Mudar para Modelo A (build na VPS4)
Docker cache invalida COPY . . perde cache? Não tocar package.json no bump
Webhook não dispara systemctl status webhook-rx Reiniciar listener ou verificar token
429 no webhook Build em progresso? Aguardar concluir
Imagens em branco nos cards Sem thumb_filename no banco? Rodar /api/images/backfill-thumbs
Clínicas não recebem update latest.yml desatualizado? Verificar se deploy-client.sh roda completo
docker load falha Imagem corrompida? Refazer build: docker compose build app

9. Changelog

Versão Data O que mudou
1.0.2 2026-05-30 RX: switch para Modelo A (build VPS4), documentar erros comuns, Wasabi storage. Deploy agora ~30-40s.
1.0.1 2026-05-30 Consolidação inicial: deploy + versionamento + segurança.
1.0.0 2026-05-30 Versão inicial.