Skip to content
bitzorcas
中EN

Guide

Deployment Method Selection

Choose between systemd single-host, docker-compose, and Kubernetes release paths, and understand migration, startup, and rollback ordering across the three Hosts.

Last updated

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

Dimensionsystemd single-hostdocker-compose single-hostKubernetes
Unitthree bitzorcas-{api,jobhost,gateway}-<env> unitsthree services api jobhost gatewayDeployment + Service + Job
Entry scriptscripts/deploy/deploy.sh; zero-downtime cutover uses deploy-blue-green.shdeploy/docker-compose.app.yml overlaydeploy/kubernetes/*.yaml
Packagingscripts/deploy/pack.* triplet, single zip with three Hostssame deploy/container/*.Dockerfile builds three imagessame images or digests as systemd/compose
Migrationdeploy.sh invokes --init-schema + --init-quartz-schema inlineone-shot migrate-schema/migrate-quartz servicesmigrate-job.yaml must complete
Config/etc/bitzorcas/<env>.env (EnvironmentFile=)deploy/app.env + compose environmentconfigmap.yaml + secret.yaml
Rollbackdeploy.sh auto-backup; blue-green uses deploy-rollback.shdocker compose down then restore image tagkubectl rollout undo
Best forsingle instance, resource-sensitive, no Docker daemonsingle-box all-in-one, reproducible, consistent customer machinesmulti-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:

Infra ready
DB / Redis / RabbitMQ

API migration
--init-schema

JobHost migration
--init-quartz-schema

Start JobHost

Start API

Start Gateway

/health/ready all green

  1. Database, Redis, and RabbitMQ are ready first.
  2. The API migrator runs --init-schema (auto CREATE DATABASE + business tables + audit partitions + idempotent seed).
  3. The JobHost migrator runs --init-quartz-schema (Quartz AdoJobStore persistence tables).
  4. Start JobHost, then API, then Gateway (Gateway reverse-proxies API and depends on API health).
  5. Each service’s /health/ready returns 200 before traffic is routed.

Stop order is the reverse: Gateway → API → JobHost.

systemd single-host deployment

Preparation

One-time server setup:

Terminal window
# ① Install .NET 10 Runtime (migrators and resident processes need it; SDK not required)
# Ubuntu example; use dnf on CentOS/RHEL
sudo apt-get install -y dotnet-runtime-10.0
dotnet --version # should print 10.0.x
# ② Create the directory layout
sudo 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.sh
sudo 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/license
cat /proc/sys/kernel/random/uuid | tr -d '-' \
| sudo tee /www/apps/<env>/.bitzorcas/license/deployment-id

Configure 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:

Terminal window
sudo cp scripts/deploy/env.example /etc/bitzorcas/<env>.env
sudo chmod 600 /etc/bitzorcas/<env>.env # contains DB passwords and JWT keys; root-only
sudo nano /etc/bitzorcas/<env>.env

Required keys are documented in scripts/deploy/env.example; the critical ones:

  • ConnectionStrings__Default: database connection string
  • PiiEncryption__SearchHashKey: openssl rand -base64 32; API and JobHost must share the same stable value
  • Jwt__Secret: openssl rand -base64 48, ≥32 chars
  • Licensing__Runtime__PolicyId: community.small.v1 (≤30-person phase) or a commercial policy
  • Frontend__BaseUrl / Cors__AllowedOrigins__0: exact browser origin (scheme + host + port)
  • Gateway__KnownProxies__0: production ingress CIDR (loopback-only rejected in Production); preview may use 127.0.0.1/8
  • ReverseProxy__Clusters__<api|signalr|files>__Destinations: cluster ids must not contain hyphens (systemd drops api-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 false
  • OpenApi__Servers__0__Url: public origin when documentation is enabled; an IP entry must include its non-default port

Seed-related variables:

VariableDefaultMeaning
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-seeddefault when unsetRuns 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_SEED0Compat: 1 → same as schema-only
BITZORCAS_SKIP_DEMO_SEED1Compat: if DEPLOY_DB_MODE unset, 1 → platform-seed, 0 → full-seed
BITZORCAS_SCHEMA_FULL_INIT01 forces full CodeFirst; default SQL Server catalog probe applies only incomplete tables/columns/indexes
USER__ADMIN__PASSWORD etc.emptyRequired only for full-seed (or SKIP_DEMO_SEED=0)

Packaging

Pack the three Hosts into a single zip (api/ jobhost/ gateway/ + manifest.txt):

Terminal window
# First deploy: force full package
./scripts/deploy/pack.sh <env> <version> --full
# Later incremental: download server baseline first, then pack without --full
scp <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

Terminal window
# Upload to the server
scp 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

EndpointExpected
systemctl status bitzorcas-api-<env>active (running)
curl localhost:8080/health/ready200
curl localhost:8081/health/ready200 (JobHost exposes only health, no business API)
curl localhost:8082/health/ready200 (Gateway, the external entry point)
curl localhost:8080/health/licensecommunity.small reports LIC.COMMUNITY_SMALL + current seats
/openapi/v1.json and /scalar/v1404 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:

Terminal window
# ① 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:

Terminal window
# 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-schema
sudo dotnet /www/apps/<env>/jobhost/BitzOrcas.JobHost.dll --init-quartz-schema
sudo 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.

Terminal window
# ① Prepare config
cp .env.example .env # infrastructure credentials
cp deploy/app.env.example deploy/app.env # app config (License/DataProtection/CORS)
# ② Build three images
docker 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 -d

depends_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/:

Terminal window
# Fill every replace-with-* placeholder in configmap.yaml
# Create bitzorcas-api-runtime Secret + a separate license envelope Secret
kubectl apply -f deploy/kubernetes/namespace.yaml
kubectl apply -f deploy/kubernetes/configmap.yaml
# The migration Job must complete successfully before applying Deployments
kubectl apply -f deploy/kubernetes/migrate-job.yaml
kubectl wait --for=condition=complete job/bitzorcas-schema-migrator -n bitzorcas
kubectl apply -f deploy/kubernetes/api.yaml
kubectl apply -f deploy/kubernetes/jobhost.yaml
kubectl apply -f deploy/kubernetes/gateway.yaml

The 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):

  1. Deploy BitzOrcas.LicenseSigner (a standalone intranet service holding the KMS/HSM private key, Bearer-authenticated only).
  2. Add the trusted public key Licensing__Runtime__TrustedPublicKeys__<key-id> to the environment config.
  3. Approve issuance via the LicenseSigner control plane with four-eyes approval; this produces runtime-license.json bound to the current deployment-id.
  4. Switch Licensing__Runtime__PolicyId to 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

SymptomFirst check
deploy.sh aborts at migrate/etc/bitzorcas/<env>.env connection string; migrator output log
API exits immediatelyjournalctl -u bitzorcas-api-<env>; often Redis/RabbitMQ unreachable or PII HMAC missing
JobHost fails to startwhether Quartz schema was migrated (--init-quartz-schema)
Gateway returns 502whether API is ready; systemctl status bitzorcas-api-<env> and API logs
/health/license returns 503license policy or seat cap exceeded; community.small caps at 30 active users
Health check timeout triggers auto-rollbackthe failing service in deploy.sh output; usually DB/Redis/MQ unreachable
compose migrator stuckmigrate-schema service logs; DB connection string or SA password
Gateway rejected in Productionwhether Gateway__KnownProxies explicitly names the ingress CIDR (loopback-only rejected)
Scalar debug calls lose the public portedge uses $http_host and sends X-Forwarded-Port; OpenAPI Server matches the public origin

See also

100%

Scroll or use controls to zoom · drag when enlarged · double-click for 100% / 200%