Pular para o conteúdo principal

DevOps & Deploy

Tudo de deploy vive em DevOps/. Existem dois caminhos distintos — não confunda:

CaminhoPastaO que é
Stack H1 (atual)DevOps/docker/Sobe o stack via imagens GHCR + docker compose, com verbos profile-aware (license / hsbr / all). É o "como faço deploy".
Legado (1 app)DevOps/deploy.sh + DevOps/lib/Deploy genérico de um único app Node/Next via PM2 + Nginx. Não faz parte do stack H1 — ignore para o stack.
dica

Use sempre os scripts em DevOps/docker/scripts/ (ex.: ./scripts/deploy.sh), não o docker compose ... cru. Os scripts cuidam de --env, login no GHCR, ordem de subida e validação de variáveis.

Modelo

  • As imagens vêm do GHCR (ghcr.io/hasarbrasildesenvolvimento/...) na tag configurada (IMAGE_TAG, default main; ou tags por serviço / branch cliente-*).
  • O Postgres roda no host (não em container) e é alcançado via host.docker.internal. Containers não criam bancos — você cria antes.
  • Profiles: license (migrator + server + worker), hsbr (back + front), all (tudo no mesmo host). Em produção o modelo comum é license dedicado num host + grupos H1 Tech apontando para ele via LICENSE_SERVER_URL público.
  • O front recebe a URL do back em runtime (NEXT_PUBLIC_URL_BASE_APP), então uma mesma imagem serve qualquer ambiente.

Infraestrutura (onde rodamos)

Rodamos num VPS da Hostinger que é compartilhado — ele hospeda vários projetos ao mesmo tempo. Por isso a regra de ouro: não derrube nem reconfigure o que já está lá (outros apps, sites). Convivem dois mundos no mesmo host:

O quêComo rodaProxy
Apps legados / outros projetosPM2 (processos Node)Nginx (vhost por domínio)
Stack H1 (license + app H1 Tech)Docker (docker compose via os scripts), publicado só no loopback (PUBLISH_BIND=127.0.0.1)Nginx (mesmo Nginx, proxy_pass127.0.0.1:<porta>)
  • O Nginx é único e compartilhado: ele faz o front (TLS + reverse proxy) tanto dos apps PM2 quanto dos containers Docker. O setup-nginx.shadiciona vhosts H1 (/etc/nginx/sites-available/h1-<env>-*) — não mexe nos vhosts existentes.
  • Os containers H1 escutam só em 127.0.0.1 (loopback); quem expõe pra internet é o Nginx. Isso evita furar o firewall (UFW) e conflitar com os outros serviços.
  • Postgres roda no host (não em container) e atende todo mundo.
  • Antes de mudanças arriscadas: tire um snapshot da Hostinger (rollback fácil).

Autorização: GitHub Packages (containers + SDK)

As imagens do stack são privadas no GHCR (GitHub Container Registry), e o SDK de licença (@hasarbrasildesenvolvimento/h1-sdk) é um pacote npm privado no GitHub Packages. Os dois usam o mesmo token: um GitHub PAT (classic) com escopo read:packages.

Onde o token entra:

Para quêOndeComo é usado
Puxar imagens do GHCRDevOps/docker/.env.secretsGITHUB_NPM_TOKENos scripts fazem docker login ghcr.io automaticamente (ensure_registry_login)
Buildar a imagem (npm ci do SDK)mesmo GITHUB_NPM_TOKENpassado como secret de build (--secret github_token)
Instalar o SDK localmente (dev/build fora do Docker).npmrc (//npm.pkg.github.com/:_authToken=...) ou GITHUB_TOKEN no ambientenpm ci lê o .npmrc

Criar o token: GitHub → Settings → Developer settings → Tokens (classic) → escopo read:packages. Depois:

# Deploy (Docker): coloque no .env.secrets do host
cp .env.secrets.example .env.secrets # GITHUB_NPM_TOKEN=ghp_xxx ; GHCR_USER=hasarbrasildesenvolvimento
chmod 600 .env.secrets

# Dev local (instalar o SDK): exporte uma vez (Windows/WSL)
# setx GITHUB_TOKEN ghp_xxx (Windows)
# echo 'export GITHUB_TOKEN=ghp_xxx' >> ~/.bashrc (WSL/Linux)

Se o docker login/pull falhar com 401/403, o token expirou ou não tem read:packages. Se o npm ci falhar baixando @hasarbrasildesenvolvimento/*, é o mesmo problema no .npmrc/GITHUB_TOKEN.

Primeiro deploy (stack completo, host novo)

Pré-requisitos no VPS: Docker Engine (com compose), Postgres no host, e DNS já apontando para o VPS se for usar HTTPS.

# 1. Crie os bancos no Postgres do host (idempotente). Cria h1_license + ga6.
psql -U postgres -h localhost -f DevOps/docker/init-dbs.sql

cd DevOps/docker

# 2. Segredos compartilhados (uma vez por host) — token do GHCR.
cp .env.secrets.example .env.secrets
# preencha GITHUB_NPM_TOKEN (read:packages) e GHCR_USER
chmod 600 .env.secrets

# 3. Env do ambiente.
cp .env.example .env # COMPOSE_PROFILES=all por padrão
# preencha os OBRIGATÓRIOS (veja a tabela abaixo)

# 4. Deploy (puxa imagens do GHCR; o migrator roda migrate+seed+keygen no 1º boot).
./scripts/deploy.sh # usa ./.env (ou: ./scripts/deploy.sh --env homolog)

# 5. (só ga6, banco novo/vazio) seed inicial uma vez:
# HSBR_RUN_SEED=1 ./scripts/deploy.sh --seed → aguarde ficar verde → volte para 0

# 6. Nginx + TLS (precisa de DNS já apontando):
sudo ./scripts/setup-nginx.sh --env <env> --certbot

# 7. Backups diários do Postgres:
sudo ./scripts/install-backup-cron.sh

# 8. Verifique:
./scripts/ps.sh --env <env>
curl http://127.0.0.1:4001/health

As migrations são automáticas: o license-migrator (one-shot) roda prisma migrate deploy + seed + keygen para h1_license, e o hsbr-back roda prisma migrate deploy para ga6 no boot. Você só roda migrate.sh manualmente para re-seed ou cenários de banco limpo.

Atualizar (rotina)

# Puxa a imagem mais nova da tag configurada e recria o que mudou:
./scripts/update.sh --env <env>

# Fixar/mover para uma tag específica (branch / sha-… / vX.Y.Z) e aplicar:
./scripts/set-branch.sh --env <env> v1.4.0 --update

Topologias: license dedicado vs grupos H1 Tech

O COMPOSE_PROFILES decide o que sobe:

  • Modelo A — tudo num host (all): license server + back/front do H1 Tech juntos. Simples; bom para homolog/single-tenant. O back fala com o license pela rede interna do compose (http://license-server:3000), então LICENSE_SERVER_URL pode ficar vazio.
  • Modelo B — license dedicado + grupos (produção): um host roda só o license server (profile=license) com domínio público + HTTPS; cada cliente/grupo roda só os serviços do H1 Tech (profile=hsbr), possivelmente em outro host, apontando para o license via LICENSE_SERVER_URL público. Novos grupos: ./scripts/new-group.sh <grupo> (cria .env.<grupo> + banco + portas; não sobe containers).

Diferença prática ao deployar:

  • License server precisa de LICENSE_AUTH_SECRET, POSTGRES_PASSWORD, domínio
    • LICENSE_AUTH_URL público (senão o login Auth.js quebra atrás do proxy). Migrations/seed/keygen rodam no 1º boot (migrator).
  • H1 Tech (back/front) precisa de HSBR_BACK_JWT_SECRET, HSBR_LICENSE_KEY (casando uma licença no servidor), NEXT_PUBLIC_URL_BASE_APP e — no profile hsbrLICENSE_SERVER_URL. O deploy.sh checa o /api/health do license antes de subir o H1 Tech (salvo --skip-license-check).

Tipos de licença no deploy (online / hybrid / offline)

O modo é escolhido por env no back do H1 Tech (detalhe completo em Licenciamento):

  • OnlineLICENSE_SERVER_URL + LICENSE_KEY. O back ativa e dá heartbeat contra o license server por HTTPS. Use quando o deploy alcança o servidor.
  • Hybrid — mesmas vars do online; a diferença é que o lease fica em cache e o back continua funcionando se o servidor ficar momentaneamente inacessível (resiliência). "Hybrid" é uma marcação no registro da licença, não outra config de env.
  • Offline / on-premiseLICENSE_FILE_PATH (+ chave pública). O back lê um .lic assinado localmente, sem rede. Para air-gapped / clientes on-premise (e é o encaixe típico do deploy SQL Server). O Integration Hub (pg-boss) fica desligado; em air-gap puro, nenhum uso é reportado.

PM2 + rotação de logs (apps legados)

Os apps não-Docker (legados/outros projetos no mesmo VPS) rodam sob PM2. O deployer legado (DevOps/deploy.sh) sobe assim: PORT=<porta> pm2 start npm --name <app> -- start + pm2 save (e pm2 startup para sobreviver a reboot); o Nginx faz o proxy :80 → localhost:<porta>.

Rotação de logs — crítico num VPS compartilhado. Sem rotação, os logs do PM2 (~/.pm2/logs/*.log) crescem sem limite e podem encher o disco do host (que é compartilhado com os outros projetos). Usamos o módulo pm2-logrotate:

pm2 install pm2-logrotate
pm2 set pm2-logrotate:max_size 10M # rotaciona ao passar de 10 MB
pm2 set pm2-logrotate:retain 14 # mantém 14 arquivos
pm2 set pm2-logrotate:compress true # comprime os antigos (.gz)
pm2 set pm2-logrotate:rotateInterval '0 0 * * *' # checa diariamente

Os logs dos containers Docker não passam pelo PM2 — quem rotaciona é o Docker (driver json-file). Limite no daemon (/etc/docker/daemon.jsonlog-opts max-size/max-file) para não encher o disco também.

Atalhos simples (comece por aqui)

Para o dia a dia, três scripts com um argumento (o <env>) resolvem quase tudo — o engine é lido do próprio .env.<env> (DB_ENGINE):

ScriptFaz
./scripts/deploy-stack.sh <env>Sobe a stack (front+back, e licença se o profile pedir) do ambiente, das imagens. == deploy.sh --env <env>. Engine automático.
./scripts/db-postgres.sh <env>(Opcional) sobe um container Postgres + cria h1_license+ga6. Só se você não tem Postgres no host.
./scripts/db-sqlserver.sh <env>(Opcional) sobe um container SQL Server 2019 + cria o banco do H1 Tech. Para o deploy SQL Server.

Bancos ficam em scripts separados de propósito — muitas vezes você aponta para um Postgres/SQL Server já existente e não precisa subir container de DB.

Sem DNS: acesso por IP:porta (teste)

Tenant novo, ainda sem domínio? Sirva pelo IP público + porta via nginx, mantendo os containers em loopback.

Não use PUBLISH_BIND=0.0.0.0

Docker gerencia o iptables e ignora o UFW — os ports ficariam abertos para a internet inteira (incluindo, se você re-rodar os db-*.sh, o banco). O caminho certo é o nginx escutar nas portas públicas e proxyar para o loopback.

  1. Escolha duas portas livres (ex.: 8081 front, 8082 back): ss -ltn | grep -E ':808[12]' (vazio = livre).
  2. Vhost por porta em /etc/nginx/sites-available/<env>-test.conf:
server {
listen 8081; # front
server_name _;
location / {
proxy_pass http://127.0.0.1:3081; # HSBR_FRONT_HOST_PORT
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-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
server {
listen 8082; # back
server_name _;
client_max_body_size 50m;
location / {
proxy_pass http://127.0.0.1:4080; # HSBR_BACK_HOST_PORT
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
ln -sf /etc/nginx/sites-available/<env>-test.conf /etc/nginx/sites-enabled/
nginx -t && systemctl reload nginx
ufw allow 8081/tcp && ufw allow 8082/tcp # portas do nginx respeitam o UFW
  1. Aponte o front para o back via nginx no .env.<env> e re-deploye:
NEXT_PUBLIC_URL_BASE_APP=http://<ip-do-vps>:8082
./scripts/deploy-stack.sh <env>
  1. Navegue em http://<ip-do-vps>:8081.
O bug clássico: cache do __env.js

Se o front carregar mas as chamadas de API forem para uma URL antiga (ex.: localhost:4080), o navegador está com o __env.js cacheado. Hard refresh (Ctrl+Shift+R) ou janela anônima. Para conferir o que está sendo servido: curl -s http://<ip>:8081/__env.js e docker exec <front> cat /app/public/__env.js.

Quando o DNS chegar: preencha HSBR_*_DOMAIN no env, rode sudo ./scripts/setup-nginx.sh --env <env> --certbot, troque o NEXT_PUBLIC_URL_BASE_APP para o domínio HTTPS, re-deploye, apague o vhost de teste e feche 8081/8082 no UFW.

Alternativa zero-exposição (nada público): túnel SSH da sua máquina — ssh -p <porta-ssh> -L 3081:127.0.0.1:<front> -L 4080:127.0.0.1:<back> user@<ip> com NEXT_PUBLIC_URL_BASE_APP=http://localhost:4080.

Referência dos verbos

Todos ficam em DevOps/docker/scripts/, aceitam --env <nome> (→ .env.<nome>), --env-file <path> e -h/--help, e carregam .env.secrets + o env do ambiente.

VerboPara quêExemplo
deploy-stack.shAtalho: sobe a stack do <env> (engine lido do .env). Wrapper fino do deploy.sh../scripts/deploy-stack.sh acme
db-postgres.sh / db-sqlserver.shSobem um container de banco (Postgres / SQL Server) + criam os DBs. Opcionais../scripts/db-sqlserver.sh --env acme
deploy.shSobe o stack conforme COMPOSE_PROFILES, na ordem certa. Puxa imagens (--build builda do fonte). Em SQL Server (DB_ENGINE=sqlserver) layeria o override automaticamente. Flags: --only license|hsbr, --seed, --no-worker../scripts/deploy.sh --env acme
update.shAtualiza (pull + recreate). --build = path legado (git pull dos repos + build). --dry-run../scripts/update.sh --env homolog
set-branch.shGrava a tag de imagem no .env.<env>. --service all|license|back|front, --deploy/--update../scripts/set-branch.sh --env acme cliente-acme --service back --deploy
migrate.shMigrations/seed manuais por profile. --fresh, --seed-only, --force../scripts/migrate.sh --env license --seed-only
backup-db.shpg_dump -Fc dos bancos do ambiente (container efêmero postgres:16-alpine)../scripts/backup-db.sh --env acme --keep 30
setup-nginx.shRenderiza/instala vhosts Nginx + (opcional) Let's Encrypt. Roda como root.sudo ./scripts/setup-nginx.sh --env homolog --certbot
install-backup-cron.shInstala cron de backup por ambiente. Roda como root.sudo ./scripts/install-backup-cron.sh
ps.sh / logs.sh / down.shStatus / logs / derrubar (down.sh --volumes apaga dados)../scripts/ps.sh --env acme
new-group.shProvisiona env+DB+portas de um grupo H1 Tech novo (não sobe containers)../scripts/new-group.sh acme

Variáveis de ambiente (checklist)

Três arquivos em DevOps/docker/, copiados dos *.example:

.env.secrets (uma vez por host, chmod 600)

  • GITHUB_NPM_TOKEN — PAT com read:packages (para buildar e puxar imagens privadas do GHCR).
  • GHCR_USER — usuário de login (default hasarbrasildesenvolvimento).

.env / .env.<env> — OBRIGATÓRIOS (o require_var rejeita vazio ou change-me*):

VarUsado porNota
LICENSE_AUTH_SECRETlicense≥32 chars (openssl rand -base64 32)
POSTGRES_PASSWORDtodosigual ao Postgres do host
HSBR_BACK_JWT_SECREThsbr-back
HSBR_LICENSE_KEYhsbr-backprecisa casar com uma license.key
NEXT_PUBLIC_URL_BASE_APPhsbr-frontURL pública do back (injetada em runtime)

Condicionais: CERTBOT_EMAIL (com --certbot); LICENSE_SERVER_URL (profile hsbr — URL pública do license; vazio no all); LICENSE_AUTH_URL (URL HTTPS pública em produção).

Com default (resumo): COMPOSE_PROFILES=all, IMAGE_TAG=main, PUBLISH_BIND=127.0.0.1, POSTGRES_HOST=host.docker.internal, HSBR_DB_NAME=ga6, LICENSE_DB_NAME=h1_license, portas LICENSE_HOST_PORT=3000 / HSBR_BACK_HOST_PORT=4001 / HSBR_FRONT_HOST_PORT=3001, BACKUP_*.

Postgres ou SQL Server

O stack suporta os dois. Postgres é o padrão; para SQL Server basta DB_ENGINE=sqlserver (+ DB_MSSQL_*) no .envdeploy.sh/deploy-stack.sh layeram o override e usam a imagem …-sqlserver automaticamente. Runbook completo em Bancos Suportados → Deploy em SQL Server.