Implantação On-Premise (Air-gap)
Esta página é a fonte de verdade para entregar o Gestão de Ativos em um ambiente do cliente — incluindo o caso mais restritivo, sem acesso à internet (air-gap). Ela amarra três coisas que vivem em páginas separadas:
- como a licença offline funciona → Licenciamento
- como o back roda em SQL Server → Bancos Suportados
- como o deploy online (GHCR) funciona → DevOps & Deploy
A diferença para o deploy online é uma só, mas é estrutural: on-premise não pode
docker pull. As imagens são privadas no GHCR e o cliente normalmente não tem
(nem queremos dar) o token. Então em vez de puxar do registry, empacotamos as
imagens num tarball (docker save) e o instalador as carrega localmente
(docker load). Nada de rede, nada de token no cliente.
Air-gap + on-premise quase sempre vem junto com SQL Server e licença
.lic (sem heartbeat). Esta página assume esse trio como caminho principal; os
demais formatos (Windows, sem Docker) estão no fim.
Matriz de entrega
| Ambiente | Formato recomendado | Observação |
|---|---|---|
| Linux | Docker / Compose (bundle) | Principal. Padronizado, auditável, update/rollback simples. É o que esta página detalha. |
| Windows Server | .exe / .msi wrapper | Instalador que garante Docker e roda o mesmo bundle. Não reempacotamos a app nativamente (veja a ressalva do SDK). |
| Linux sem Docker | .tar.gz + systemd | Último recurso, quando a política do cliente proíbe Docker. Exige node_modules pré-instalado no pacote (SDK privado). |
Anatomia do bundle
O lado-build (nossa máquina ou CI, com rede) produz um único arquivo
h1-onprem-<versao>.tar.gz:
h1-onprem-v1.8.0/
├── images.tar # docker save de TODAS as imagens (inclui SQL Server 2019)
├── docker-compose.yml # stack base (DevOps/docker)
├── docker-compose.sqlserver.yml # override do engine SQL Server (já existe)
├── docker-compose.offline.yml # override air-gap: licença .lic + SQL Server na mesma rede
├── .env # pré-preenchido (secrets já gerados)
├── license/
│ ├── license.lic # token JWT cru assinado (Ed25519)
│ └── public-key.json # JWK de verificação (extraída do .lic; ver nota abaixo)
├── _onprem-lib.sh # helpers compartilhados (install/update)
├── install.sh # roda no cliente: load + up + healthcheck
├── update.sh # troca a versão (load nova + recreate)
├── README.md # guia do cliente
└── MANIFEST # versão + sha256 de cada imagem (auditoria)
images.tarPara o instalador ser auto-suficiente, o mcr.microsoft.com/mssql/server:2019-latest
também é salvo no tarball. Se o cliente já tem um SQL Server licenciado e
gerenciado, dá para não embutir o container — veja
SQL Server: embutido vs gerenciado.
license.lic (+ public-key.json)O .lic baixado pelo "Baixar .lic" do admin UI é um envelope JSON
{ token, publicKey } que embute a chave. O SDK @hasarbrasildesenvolvimento/h1-sdk
≥ 0.5.0 lê esse envelope nativamente (verifica com a chave embutida, sem
arquivo extra) — validado: um .lic envelope ativa com status: active sem
public-key.json.
Porém, a imagem do back só passa a usar 0.5.0 depois de um rebuild (ela
assa o SDK em build time; até lá roda 0.4.0, que não lê o envelope). Para o
bundle funcionar independente da versão da imagem, o build-bundle.sh
normaliza todo envelope: decompõe em license.lic (JWT cru) + public-key.json
(a chave embutida) e preenche LICENSE_PUBLIC_KEY_PATH. Isso roda na imagem
0.4.0 atual e na 0.5.0+ (a chave explícita tem prioridade sobre a embutida),
então a normalização fica como padrão seguro mesmo após o rebuild. Para um .lic
já cru (CLI export-lic-file.ts) sem --public-key, o script avisa que o
back não conseguirá verificar.
O que dispensa o air-gap puro
Em modo offline .lic, o back lê a licença do disco e verifica a assinatura
localmente. Consequência prática: não sobem no box do cliente o
license-server, o license-worker nem o banco h1_license. O bundle on-premise
é só SQL Server + hsbr-back + hsbr-front + o .lic. O Integration Hub
(pg-boss) fica desligado (EVENTS_DATABASE_URL=""); em air-gap puro nenhum uso é
reportado de volta.
Override air-gap (docker-compose.offline.yml)
Layer no topo do docker-compose.yml + docker-compose.sqlserver.yml. Faz duas
coisas: aponta a licença para os arquivos locais e coloca o SQL Server na mesma
rede do compose (o back fala com ele pelo nome do serviço, evitando a
pegadinha do host.docker.internal + UFW que existe quando o banco está no host).
name: h1-stack
services:
sqlserver:
image: mcr.microsoft.com/mssql/server:2019-latest
restart: unless-stopped
environment:
ACCEPT_EULA: "Y"
MSSQL_SA_PASSWORD: ${DB_MSSQL_SA_PASSWORD:?defina no .env}
MSSQL_PID: ${MSSQL_PID:-Developer} # ⚠️ Developer NÃO é licenciado p/ produção
volumes:
- h1-mssqldata:/var/opt/mssql
healthcheck:
test: ["CMD-SHELL", '/opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P "$$MSSQL_SA_PASSWORD" -C -Q "SELECT 1" || exit 1']
interval: 10s
timeout: 5s
retries: 24
start_period: 60s
hsbr-back:
# back fala com o SQL Server pelo nome do serviço (mesma rede) — sem host.docker.internal
environment:
DB_HOST: sqlserver
DB_PORT: "1433"
# Licença OFFLINE: lê o .lic local, sem rede. Anula online.
LICENSE_SERVER_URL: ""
LICENSE_KEY: ""
LICENSE_FILE_PATH: /app/offline-license/license.lic
# Vazio = usa a chave EMBUTIDA no .lic (envelope do admin UI).
# Preenchido pelo .env (quando se passa --public-key) = verificação pinada.
LICENSE_PUBLIC_KEY_PATH: ${LICENSE_PUBLIC_KEY_PATH:-}
depends_on:
sqlserver:
condition: service_healthy
db-init:
condition: service_completed_successfully
volumes:
- ./license:/app/offline-license:ro # monta o .lic (e public-key.json, se houver)
volumes:
h1-mssqldata:
-sqlserver é obrigatóriaO client Prisma é assado por engine no build. A imagem do back tem que ser a
de sufixo …-sqlserver (ex.: hsbr-gestao-ativos-back:v1.8.0-sqlserver). A tag
padrão é Postgres e não roda contra SQL Server. Detalhe em
Bancos Suportados.
.env do bundle (offline + SQL Server)
Gerado já preenchido pelo build-bundle.sh. Note a ausência de
LICENSE_SERVER_URL/LICENSE_KEY (offline) e de GITHUB_NPM_TOKEN (não há pull
no cliente).
H1_ENV=<cliente>
COMPOSE_PROFILES=hsbr
DB_ENGINE=sqlserver
DB_MSSQL_HOST=sqlserver # nome do serviço na rede do compose
PUBLISH_BIND=0.0.0.0 # acesso na LAN interna (air-gap); veja aviso de rede
TZ=America/Sao_Paulo
# --- SQL Server (container embutido) ---
DB_MSSQL_SA_PASSWORD=<gerado>
HSBR_DB_NAME=ga6
MSSQL_PID=Developer # ⚠️ não-licenciado p/ produção
# --- HSBR backend ---
HSBR_BACK_IMAGE_TAG=v1.8.0 # vira v1.8.0-sqlserver via override
HSBR_BACK_JWT_SECRET=<gerado>
LICENSE_PRODUCT=HSBR_ALL1 # casa com o claim `aud` do .lic
LICENSE_PUBLIC_KEY_PATH= # vazio = chave embutida no .lic
# --- Seed mínimo (só com --seed) — cria o admin no install ---
HSBR_SEED_MINIMAL=1
HSBR_ADMIN_EMAIL=admin@cliente.com
HSBR_ADMIN_PASSWORD=<gerado/definido>
# --- HSBR frontend ---
NEXT_PUBLIC_URL_BASE_APP=http://<ip-ou-dominio-do-cliente>:4001
.lic, não de uma keyNo modo offline não há HSBR_LICENSE_KEY — o backend casa o LICENSE_PRODUCT
(env) com o claim aud do .lic; limites e features vêm dos claims do próprio
arquivo. As vars de licença online (LICENSE_SERVER_URL/LICENSE_KEY) ficam
vazias para o detectMode() escolher offline.
Lado-build: gerar o bundle
Roda na nossa infra, com acesso ao GHCR. Tudo num só comando —
DevOps/docker/scripts/build-bundle.sh:
cd DevOps/docker
# Caso típico: o .lic veio do "Baixar .lic" do admin UI (embute a chave)
# + seed mínimo (cria o usuário admin no primeiro install)
./scripts/build-bundle.sh \
--tag v1.8.0 \
--license ./acme.lic \
--back-url http://10.0.0.5:4001 \
--env-name acme \
--seed --admin-email admin@acme.com --admin-password 'TroqueIsto!'
# Verificação pinada (chave separada) ou .lic gerado pelo CLI (JWT cru):
./scripts/build-bundle.sh --tag v1.8.0 --license ./acme.lic \
--public-key ./public-key.json --back-url http://10.0.0.5:4001
# Teste do pipeline sem imagens nem licença real:
./scripts/build-bundle.sh --tag v1.8.0 --fake-license --no-images
O script faz, em ordem: docker login + pull das 3 imagens (back
-sqlserver, front, SQL Server 2019) → docker save num images.tar →
copia compose + overrides + scripts do cliente → renderiza o .env (secrets
gerados via openssl) → embute a licença → escreve o MANIFEST (sha256 das
imagens) → empacota o .tar.gz. Flags úteis: --seed (+ --admin-email/
--admin-password — cria o admin no install), --skip-pull (imagens já locais),
--no-images (só monta a pasta), --bind, --sa-password, --mssql-pid,
--license-key <uuid> (gera o .lic via h1-license-server — JWT cru, requer
--public-key). Saída em DevOps/docker/dist/.
Lado-cliente: instalar (offline)
O cliente só precisa de Docker Engine + compose (nada de curl, node ou
ferramentas de dev). Nenhum pull, nenhum token.
tar xzf h1-onprem-v1.8.0.tar.gz && cd h1-onprem-v1.8.0
sudo ./install.sh
O install.sh é idempotente (re-rodar é seguro) e falha com mensagem clara
antes de quebrar: confere docker/daemon/permissão, valida o sha256 do
images.tar contra o MANIFEST (transferência corrompida), confirma que as
imagens carregaram, avisa sobre porta ocupada / disco baixo, e espera o
healthcheck do container (sem depender de curl no host).
O install.sh faz:
docker load -i images.tar # carrega as imagens do tarball
docker compose \
-f docker-compose.yml \
-f docker-compose.sqlserver.yml \
-f docker-compose.offline.yml \
--env-file .env up -d # SQL Server → back → front
# espera o /health e imprime a URL de acesso
curl -fsS http://127.0.0.1:4001/health && echo " OK"
As migrations são automáticas: o hsbr-back roda prisma migrate deploy
contra o ga6 no boot (a primeira subida cria o schema). Não há passo manual
obrigatório.
Login inicial (seed mínimo)
Se o bundle foi gerado com --seed, o install.sh roda o seed mínimo no back
já saudável — cria o usuário admin (de --admin-email/--admin-password) +
statuses/configs/feature-flags, sem fixtures de teste — e imprime o login no
fim. É idempotente (upsert); re-rodar não duplica nada. Sem --seed, nenhum
usuário é criado (você cadastra depois). Para (re)rodar manualmente:
./install.sh seed
Operar
./install.sh ps # status dos serviços
./install.sh logs hsbr-back # logs de um serviço
./install.sh restart # recriar a partir das imagens atuais
./install.sh down # parar (MANTÉM os dados)
./install.sh down -v # ⚠️ parar e APAGAR os volumes (zera o banco)
Bundle real (.lic envelope + imagens develop 0.5.0) extraído e instalado num
host limpo via install.sh. Resultado: os 4 containers sobem saudáveis
(sqlserver · db-init cria o ga6 · hsbr-back migra + ativa a licença ·
hsbr-front), o seed cria o admin, e o login funciona:
Integridade OK ✔ → docker load (3 imagens) ✔ → imagens verificadas ✔
sqlserver Healthy · db-init Exited(0) · hsbr-back Healthy · hsbr-front Healthy
[license] offline license loaded — product=HSBR_ALL1 tenant=Empresa Teste LTDA expires=2027-06-25
Seeded admin user: admin@example.com
POST /user/signin → HTTP 200 + JWT
GET /health → {"status":"ok"}; front responde HTTP 200.
Ciclo de vida e reboot
Instala uma vez. Os dados ficam em volumes Docker nomeados que sobrevivem a restart, reboot e atualização — você não reinstala:
| Volume | Conteúdo |
|---|---|
h1-mssqldata | o banco SQL Server (ga6) |
hsbr-back-uploads | arquivos enviados |
hsbr-back-license | cache da licença |
Restart / reboot do servidor. Os serviços usam restart: unless-stopped, então
o Docker religa os containers sozinho quando eles caem e quando o daemon sobe.
Para o reboot do host trazer a stack de volta automaticamente, garanta que o
Docker inicia no boot:
sudo systemctl enable docker
No reboot: o Docker sobe → os containers voltam (dados intactos nos volumes) → o
hsbr-back re-roda prisma migrate deploy (idempotente, no-op se nada mudou). O
db-init é one-shot e fica Exited — normal, o banco já existe.
down -v apaga os dados./install.sh down para a stack mantendo os volumes. ./install.sh down -v
remove os volumes — incluindo o banco. Só use -v para zerar de propósito.
Update
Cada release nova é um bundle novo. O fluxo é backup → enviar bundle → carregar imagens → recriar:
# 1. Cliente valida/faz backup do banco (SQL Server nativo — ver ressalva)
# 2. Envia o novo .tar.gz, extrai
tar xzf h1-onprem-v1.9.0.tar.gz && cd h1-onprem-v1.9.0
# 3. update.sh: carrega as imagens novas e recria só o que mudou
sudo ./update.sh
sudo ./update.sh --migrate # idem, mas roda a migration num passo controlado ANTES de subir
update.sh essencialmente:
docker load -i images.tar
# (opcional) migration controlada ANTES de subir, com a imagem -sqlserver:
docker compose ... run --rm hsbr-back npx prisma migrate deploy
docker compose ... up -d
O passo run --rm ... migrate deploy é opcional — serve para aplicar/auditar
migrations num passo controlado antes do up. Se você pular, o entrypoint do back
aplica no próximo boot do mesmo jeito.
Rollback
Como o bundle é versionado e as tags de imagem são fixas (nunca latest),
voltar é determinístico:
- Mantenha o
.tar.gzda versão anterior no servidor (não apague no update). cdna pasta da versão antiga e rodesudo ./install.shde novo (docker loadé idempotente; oup -drecria com as imagens antigas).- Se a versão nova rodou migrations destrutivas, restaure o backup do banco antes do passo 2.
O backup-db.sh do DevOps é pg_dump-only. Para SQL Server use um backup
nativo (.bak) antes de qualquer update. Combine a política de backup com o
cliente.
SQL Server: embutido vs gerenciado
| Cenário | Como | Quando |
|---|---|---|
| Embutido no bundle (padrão) | Container sqlserver no docker-compose.offline.yml, volume h1-mssqldata | Cliente quer caixa-fechada; aceita MSSQL_PID licenciado por nós/por ele |
| Gerenciado pelo cliente | Remove o serviço sqlserver do override; aponta DB_HOST/DB_PORT para a instância do cliente | Cliente já tem SQL Server licenciado e administrado |
MSSQL_PID=Developer não é licenciado para produção. Para caixa-fechada em
produção, defina uma edição licenciada (Standard/etc.) via MSSQL_PID, ou use o
SQL Server gerenciado do cliente. SQL Server 2019 quer ~2GB+ de RAM.
Windows e sem Docker
- Windows Server (
.msi): entregamos um instalador que garante o Docker (Desktop/Engine) e roda o mesmo bundle acima. Não mantemos um segundo empacotamento nativo da aplicação. - Sem Docker (
.tar.gz+ systemd): viável, porém o back depende do SDK privado@hasarbrasildesenvolvimento/h1-sdk(GitHub Packages). Sem Docker e sem internet,npm ciquebra — então o pacote precisa levarnode_modulespré-instalado e o runtime Node embutido. É o formato mais caro de manter; reserve para clientes que proíbem Docker por política.
Checklist de entrega on-premise
- Licença criada no admin UI (tenant + produto + limites)
-
.licobtido via "Baixar .lic" do admin UI (envelope, embute a chave) — ou, se via CLI/pinada,public-key.jsonjunto -
build-bundle.sh --tag <ver> --license <.lic> --back-url <url>rodado (--seed --admin-email/--admin-passwordse quiser admin no 1º boot) - Imagem do back na variante
-sqlserverda tag da release (o script já usa) -
images.targerado com back + front + SQL Server 2019 -
.envrenderizado:DB_ENGINE=sqlserver,LICENSE_FILE_PATH, semLICENSE_SERVER_URL/LICENSE_KEY -
MANIFESTcom sha256 das imagens - Testado num host limpo sem rede (load + up + 4 containers healthy +
/healthverde + login JWT) -
sudo systemctl enable dockerno host (stack volta sozinha no reboot) - Política de backup (SQL Server nativo) e janela de update combinadas com o cliente