DevOps & Deploy
Tudo de deploy vive em DevOps/. Existem dois caminhos distintos — não
confunda:
| Caminho | Pasta | O 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. |
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, defaultmain; ou tags por serviço / branchcliente-*). - 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 viaLICENSE_SERVER_URLpú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 roda | Proxy |
|---|---|---|
| Apps legados / outros projetos | PM2 (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_pass → 127.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.shsó adiciona 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ê | Onde | Como é usado |
|---|---|---|
| Puxar imagens do GHCR | DevOps/docker/.env.secrets → GITHUB_NPM_TOKEN | os scripts fazem docker login ghcr.io automaticamente (ensure_registry_login) |
| Buildar a imagem (npm ci do SDK) | mesmo GITHUB_NPM_TOKEN | passado 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 ambiente | npm 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 temread:packages. Se onpm cifalhar 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ãoLICENSE_SERVER_URLpode 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 viaLICENSE_SERVER_URLpú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ínioLICENSE_AUTH_URLpú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_APPe — no profilehsbr—LICENSE_SERVER_URL. Odeploy.shcheca o/api/healthdo 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):
- Online —
LICENSE_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-premise —
LICENSE_FILE_PATH(+ chave pública). O back lê um.licassinado 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.json→log-optsmax-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):
| Script | Faz |
|---|---|
./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.
PUBLISH_BIND=0.0.0.0Docker 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.
- Escolha duas portas livres (ex.: 8081 front, 8082 back):
ss -ltn | grep -E ':808[12]'(vazio = livre). - 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
- 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>
- Navegue em
http://<ip-do-vps>:8081.
__env.jsSe 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.
| Verbo | Para quê | Exemplo |
|---|---|---|
deploy-stack.sh | Atalho: sobe a stack do <env> (engine lido do .env). Wrapper fino do deploy.sh. | ./scripts/deploy-stack.sh acme |
db-postgres.sh / db-sqlserver.sh | Sobem um container de banco (Postgres / SQL Server) + criam os DBs. Opcionais. | ./scripts/db-sqlserver.sh --env acme |
deploy.sh | Sobe 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.sh | Atualiza (pull + recreate). --build = path legado (git pull dos repos + build). --dry-run. | ./scripts/update.sh --env homolog |
set-branch.sh | Grava 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.sh | Migrations/seed manuais por profile. --fresh, --seed-only, --force. | ./scripts/migrate.sh --env license --seed-only |
backup-db.sh | pg_dump -Fc dos bancos do ambiente (container efêmero postgres:16-alpine). | ./scripts/backup-db.sh --env acme --keep 30 |
setup-nginx.sh | Renderiza/instala vhosts Nginx + (opcional) Let's Encrypt. Roda como root. | sudo ./scripts/setup-nginx.sh --env homolog --certbot |
install-backup-cron.sh | Instala cron de backup por ambiente. Roda como root. | sudo ./scripts/install-backup-cron.sh |
ps.sh / logs.sh / down.sh | Status / logs / derrubar (down.sh --volumes apaga dados). | ./scripts/ps.sh --env acme |
new-group.sh | Provisiona 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 comread:packages(para buildar e puxar imagens privadas do GHCR).GHCR_USER— usuário de login (defaulthasarbrasildesenvolvimento).
.env / .env.<env> — OBRIGATÓRIOS (o require_var rejeita vazio ou change-me*):
| Var | Usado por | Nota |
|---|---|---|
LICENSE_AUTH_SECRET | license | ≥32 chars (openssl rand -base64 32) |
POSTGRES_PASSWORD | todos | igual ao Postgres do host |
HSBR_BACK_JWT_SECRET | hsbr-back | |
HSBR_LICENSE_KEY | hsbr-back | precisa casar com uma license.key |
NEXT_PUBLIC_URL_BASE_APP | hsbr-front | URL 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_*.
O stack suporta os dois. Postgres é o padrão; para SQL Server basta
DB_ENGINE=sqlserver (+ DB_MSSQL_*) no .env — deploy.sh/deploy-stack.sh
layeram o override e usam a imagem …-sqlserver automaticamente. Runbook completo
em Bancos Suportados → Deploy em SQL Server.