The BitzOrcas production Hosts are three independent processes: API, JobHost, and Gateway. The repository does not declare any single topology as the only official path. Instead it ships scripts and manifests for three common routes — single-host systemd, single-host docker-compose, and Kubernetes. All three routes must satisfy the same invariants: migrations complete before resident services start, the three services start and stop in dependency order, and a failing /health/ready must block traffic.
Comparing the three routes
| Dimension | systemd single-host | docker-compose single-host | Kubernetes |
|---|---|---|---|
| Unit | three bitzorcas-{api,jobhost,gateway}-<env> units | three services api jobhost gateway | Deployment + Service + Job |
| Entry script | scripts/deploy/deploy.sh; zero-downtime cutover uses deploy-blue-green.sh | deploy/docker-compose.app.yml overlay | deploy/kubernetes/*.yaml |
| Packaging | scripts/deploy/pack.* triplet, single zip with three Hosts | same deploy/container/*.Dockerfile builds three images | same images or digests as systemd/compose |
| Migration | deploy.sh invokes --init-schema + --init-quartz-schema inline | one-shot migrate-schema/migrate-quartz services | migrate-job.yaml must complete |
| Config | /etc/bitzorcas/<env>.env (EnvironmentFile=) | deploy/app.env + compose environment | configmap.yaml + secret.yaml |
| Rollback | deploy.sh auto-backup; blue-green uses deploy-rollback.sh | docker compose down then restore image tag | kubectl rollout undo |
| Best for | single instance, resource-sensitive, no Docker daemon | single-box all-in-one, reproducible, consistent customer machines | multi-replica, autoscaling, fault-domain isolation |
Shared invariants
Regardless of route, the deploy flow must preserve this ordering, or migrations and startup will race and health checks will misjudge readiness:
- Database, Redis, and RabbitMQ are ready first.
- The API migrator runs
--init-schema(autoCREATE DATABASE+ business tables + audit partitions + idempotent seed). - The JobHost migrator runs
--init-quartz-schema(QuartzAdoJobStorepersistence tables). - Start JobHost, then API, then Gateway (Gateway reverse-proxies API and depends on API health).
- Each service’s
/health/readyreturns 200 before traffic is routed.
Stop order is the reverse: Gateway → API → JobHost.
systemd single-host deployment
Preparation
One-time server setup:
# ① Install .NET 10 Runtime (migrators and resident processes need it; SDK not required)# Ubuntu example; use dnf on CentOS/RHELsudo apt-get install -y dotnet-runtime-10.0dotnet --version # should print 10.0.x
# ② Create the directory layoutsudo mkdir -p /www/{apps,releases,backups,scripts} /etc/bitzorcas
# ③ Place the deploy script (from the code repo scripts/deploy/)sudo cp scripts/deploy/deploy.sh /www/scripts/deploy.shsudo chmod +x /www/scripts/deploy.sh
# ④ Generate deployment-id (community.small seat-counting anchor; once set, never change it)sudo mkdir -p /www/apps/<env>/.bitzorcas/licensecat /proc/sys/kernel/random/uuid | tr -d '-' \ | sudo tee /www/apps/<env>/.bitzorcas/license/deployment-idConfigure the environment file
/etc/bitzorcas/<env>.env is used by deploy.sh (load_env_file for migrators—do not source keys that are illegal shell identifiers) and by the three systemd units (EnvironmentFile=), so all three services share one file:
sudo cp scripts/deploy/env.example /etc/bitzorcas/<env>.envsudo chmod 600 /etc/bitzorcas/<env>.env # contains DB passwords and JWT keys; root-onlysudo nano /etc/bitzorcas/<env>.envRequired keys are documented in scripts/deploy/env.example; the critical ones:
ConnectionStrings__Default: database connection stringPiiEncryption__SearchHashKey:openssl rand -base64 32; API and JobHost must share the same stable valueJwt__Secret:openssl rand -base64 48, ≥32 charsLicensing__Runtime__PolicyId:community.small.v1(≤30-person phase) or a commercial policyFrontend__BaseUrl/Cors__AllowedOrigins__0: exact browser origin (scheme + host + port)Gateway__KnownProxies__0: production ingress CIDR (loopback-only rejected in Production); preview may use127.0.0.1/8ReverseProxy__Clusters__<api|signalr|files>__Destinations: cluster ids must not contain hyphens (systemd dropsapi-cluster)OpenApi__Enabled/OpenApi__RequireAuthentication/OpenApi__PersistAuthentication: normally disable documentation in Production; an enabled shared preview requires the product documentation session and keeps Scalar authentication persistence falseOpenApi__Servers__0__Url: public origin when documentation is enabled; an IP entry must include its non-default port
Seed-related variables:
| Variable | Default | Meaning |
|---|---|---|
BITZORCAS_DEPLOY_DB_MODE | (derived) | Preferred: schema-only / platform-seed / full-seed |
schema-only | — | --init-schema --no-seed: create/alter tables + audit partitions + CAP Outbox; no seeds at all (including dictionaries). Recommended for day-to-day app deploys |
platform-seed | default when unset | Runs platform seeds (dictionaries, modules, …) but skips demo users/roles via SkipSeedIds. Still runs dictionary seeds — not “tables only” |
full-seed | — | Platform seeds + demo tenant/users/host accounts; requires all USER__*__PASSWORD(_HASH) |
BITZORCAS_INIT_NO_SEED | 0 | Compat: 1 → same as schema-only |
BITZORCAS_SKIP_DEMO_SEED | 1 | Compat: if DEPLOY_DB_MODE unset, 1 → platform-seed, 0 → full-seed |
BITZORCAS_SCHEMA_FULL_INIT | 0 | 1 forces full CodeFirst; default SQL Server catalog probe applies only incomplete tables/columns/indexes |
USER__ADMIN__PASSWORD etc. | empty | Required only for full-seed (or SKIP_DEMO_SEED=0) |
Packaging
Pack the three Hosts into a single zip (api/ jobhost/ gateway/ + manifest.txt):
# First deploy: force full package./scripts/deploy/pack.sh <env> <version> --full
# Later incremental: download server baseline first, then pack without --fullscp <user>@<server>:/www/apps/<env>/manifest.txt \ ../publish/<branch>/manifest-server-<env>-$(date +%m%d%H%M).txt./scripts/deploy/pack.sh <env> <version>
# Windows PowerShell (UTF-8 scripts; console forced to UTF-8 at startup).\scripts\deploy\pack.ps1 -Env <env> -Version <version>.\scripts\deploy\pack-frontend.ps1 -Environment <env> -Version <version>Incremental mode trusts only manifest-server-<env>-*.txt. Local manifest-baseline-* is audit-only; without a server baseline the packer falls back to a full package. Output: <repo-parent>/publish/<branch>/bitzorcas-<env>-<version>.zip.
Deploy
# Upload to the serverscp bitzorcas-<env>-<version>.zip <user>@<server>:/www/releases/
# Default platform-seed: platform seeds (dictionaries, …), demo users skipped — not “tables only”ssh <user>@<server> "sudo /www/scripts/deploy.sh /www/releases/bitzorcas-<env>-<version>.zip <env>"
# Day-to-day app deploys: tables/columns only, no seeds at all (including dictionaries)ssh <user>@<server> "sudo BITZORCAS_DEPLOY_DB_MODE=schema-only /www/scripts/deploy.sh /www/releases/bitzorcas-<env>-<version>.zip <env>"# Compat:# ssh <user>@<server> "sudo BITZORCAS_INIT_NO_SEED=1 /www/scripts/deploy.sh /www/releases/bitzorcas-<env>-<version>.zip <env>"
# First demo environment (requires USER__*__PASSWORD):# ssh <user>@<server> "sudo BITZORCAS_DEPLOY_DB_MODE=full-seed /www/scripts/deploy.sh /www/releases/bitzorcas-<env>-<version>.zip <env>"deploy.sh flow: precheck → stop (Gateway→API→JobHost) → ports → backup → extract → write Gateway appsettings.Deploy.json → API schema (seeds depend on DEPLOY_DB_MODE) → JobHost --init-quartz-schema → systemd units → start (JobHost→API→Gateway) → health (journal dump on failure) → auto-rollback on failure → install server manifest.txt on success.
Schema: SQL Server catalog probe skips CodeFirst when complete; when only some tables/columns/indexes are missing, only affected models run InitTables; BITZORCAS_SCHEMA_FULL_INIT=1 or probe failure forces full CodeFirst. The probe checks object existence only, not column type/length.
Judging success
| Endpoint | Expected |
|---|---|
systemctl status bitzorcas-api-<env> | active (running) |
curl localhost:8080/health/ready | 200 |
curl localhost:8081/health/ready | 200 (JobHost exposes only health, no business API) |
curl localhost:8082/health/ready | 200 (Gateway, the external entry point) |
curl localhost:8080/health/license | community.small reports LIC.COMMUNITY_SMALL + current seats |
/openapi/v1.json and /scalar/v1 | 404 in Production; 200 as configured for an externally protected preview |
Logs: journalctl -u 'bitzorcas-*-<env>' -f
On a 1Panel host that needs a zero-downtime Gateway cutover, do not keep overwriting in place with deploy.sh. Wire locations to the named upstream as in 1Panel OpenResty blue-green, then:
# ① Wire the named upstream first. Rollback does not reverse schema.sudo /www/scripts/setup-1panel-openresty.sh <env>sudo BITZORCAS_DEPLOY_DB_MODE=schema-only \ /www/scripts/deploy-blue-green.sh /www/releases/bitzorcas-<env>-<version>.zip <env>sudo /www/scripts/deploy-rollback.sh <env>Rollback
deploy.sh takes an incremental backup to /www/backups/<env>/ before each deploy and auto-rolls back on health-check failure (restore backup → re-run migrations → restart). Manual rollback:
# Restore the previous release, reconcile both schemas, and only then restart traffic.sudo systemctl stop bitzorcas-gateway-<env> bitzorcas-api-<env> bitzorcas-jobhost-<env>sudo tar -xzf /www/backups/<env>/backup_<timestamp>.tar.gz -C /www/apps/<env>sudo dotnet /www/apps/<env>/api/BitzOrcas.Api.dll --init-schemasudo dotnet /www/apps/<env>/jobhost/BitzOrcas.JobHost.dll --init-quartz-schemasudo systemctl start bitzorcas-jobhost-<env> bitzorcas-api-<env> bitzorcas-gateway-<env>docker-compose single-host deployment
For single-host scenarios that prefer reproducible environments over bare process management. The repo-root docker-compose.yml orchestrates only infrastructure (SQL Server / RabbitMQ / Redis / AgileConfig / Qdrant); the application stack is added by the deploy/docker-compose.app.yml overlay.
# ① Prepare configcp .env.example .env # infrastructure credentialscp deploy/app.env.example deploy/app.env # app config (License/DataProtection/CORS)
# ② Build three imagesdocker build -f deploy/container/Dockerfile -t bitzorcas-api:local .docker build -f deploy/container/JobHost.Dockerfile -t bitzorcas-jobhost:local .docker build -f deploy/container/Gateway.Dockerfile -t bitzorcas-gateway:local .
# ③ Start (migrator services run first; api/jobhost/gateway start after they complete)docker compose -f docker-compose.yml -f deploy/docker-compose.app.yml up -ddepends_on + condition: service_completed_successfully guarantees migrators finish before resident services. The first phase (community.small) uses ASPNETCORE_ENVIRONMENT=Development because Production is gated by RuntimeConfigurationGuard requiring the full production dependency set (see production-configuration).
Kubernetes deployment
For production scenarios needing multi-replica, autoscaling, and fault-domain isolation. Manifests live in deploy/kubernetes/:
# Fill every replace-with-* placeholder in configmap.yaml# Create bitzorcas-api-runtime Secret + a separate license envelope Secretkubectl apply -f deploy/kubernetes/namespace.yamlkubectl apply -f deploy/kubernetes/configmap.yaml
# The migration Job must complete successfully before applying Deploymentskubectl apply -f deploy/kubernetes/migrate-job.yamlkubectl wait --for=condition=complete job/bitzorcas-schema-migrator -n bitzorcas
kubectl apply -f deploy/kubernetes/api.yamlkubectl apply -f deploy/kubernetes/jobhost.yamlkubectl apply -f deploy/kubernetes/gateway.yamlThe full K8s contract is in deploy/kubernetes/README.md: runtime replicas never apply schema at startup, readOnlyRootFilesystem, deployment-id must be identical across replicas, and PII HMAC rotation does not support rolling deploys.
License transition
All three routes use the same procedure to switch from community.small (≤30 active natural-person users, no signed License) to a commercial License (full Platform capabilities):
- Deploy
BitzOrcas.LicenseSigner(a standalone intranet service holding the KMS/HSM private key, Bearer-authenticated only). - Add the trusted public key
Licensing__Runtime__TrustedPublicKeys__<key-id>to the environment config. - Approve issuance via the LicenseSigner control plane with four-eyes approval; this produces
runtime-license.jsonbound to the currentdeployment-id. - Switch
Licensing__Runtime__PolicyIdto the commercial policy and restart API and JobHost.
After the switch /health/license moves from LIC.COMMUNITY_SMALL to Valid and the 30-seat cap is lifted. See runtime license issuance.
Common failures
| Symptom | First check |
|---|---|
| deploy.sh aborts at migrate | /etc/bitzorcas/<env>.env connection string; migrator output log |
| API exits immediately | journalctl -u bitzorcas-api-<env>; often Redis/RabbitMQ unreachable or PII HMAC missing |
| JobHost fails to start | whether Quartz schema was migrated (--init-quartz-schema) |
| Gateway returns 502 | whether API is ready; systemctl status bitzorcas-api-<env> and API logs |
/health/license returns 503 | license policy or seat cap exceeded; community.small caps at 30 active users |
| Health check timeout triggers auto-rollback | the failing service in deploy.sh output; usually DB/Redis/MQ unreachable |
| compose migrator stuck | migrate-schema service logs; DB connection string or SA password |
| Gateway rejected in Production | whether Gateway__KnownProxies explicitly names the ingress CIDR (loopback-only rejected) |
| Scalar debug calls lose the public port | edge uses $http_host and sends X-Forwarded-Port; OpenAPI Server matches the public origin |