Files
BizGaze_Remote/DEPLOY.md
T
Sravan 472170784b fix(livekit): single UDP media port + pin node_ip for the NAT'd server
The prod box is behind NAT (private 192.168.88.61 behind public 118.95.33.89), so
LiveKit auto-IP-detection would pick the wrong (outbound) address. Pin
rtc.node_ip=118.95.33.89 and collapse media to one UDP port (50000) + TCP 7881 to
minimize the upstream gateway port-forward the network team must add. Docs updated
with the exact forward table.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 13:50:24 +05:30

13 KiB

Deploying BizGaze Support

The app runs as a Docker container behind the existing Nginx Proxy Manager, which terminates TLS and proxies https://remote.bizgaze.combizgaze-support:8090 on the shared nginx_proxy_manager_default network. No host ports are published.

Deployment model: the server holds a git clone of this repo. Each deploy is a git pull + rebuild via deploy.sh. Two files are not in git and live only on the server — they survive every pull:

File Purpose
.env Secrets — TURN credentials, optional SSO_SECRET, BIZGAZE_WEBHOOK_URL. See .env.example.
server/cert.pem, server/key.pem Self-signed cert for the app's optional direct-HTTPS listener (8443). Not needed behind NPM, but harmless.

Server facts:

  • Host: root@118.95.33.89 port 61
  • App path: /opt/bizgaze-support
  • Data: Docker named volume bizgaze_support_data/data/data.db (persists across rebuilds)
  • Backups: /opt/bizgaze-support.backups/ (newest 3 .tgz snapshots, auto-rotated)

Operational guardrails (read before every deploy)

These are correctness/security invariants, not preferences. Breaking one degrades or breaks the app even if the container starts fine.

  • Single instance only. Chat, presence, and meeting (WebRTC) signaling use an in-process registry. Do not scale to multiple replicas or place several instances behind a round-robin load balancer — users on different processes can't see each other's messages/calls. One container, one process.
  • ALLOW_LOCAL_LOGIN must NOT be set in production. It's a dev-only escape hatch that bypasses BizGaze SSO and the local-password lockout. Production logs in via BizGaze only.
  • BIZGAZE_DIRECTORY_TOKEN is server-side only — it's used by the server to proxy directory lookups and must never be exposed to the browser/client.
  • HTML is served Cache-Control: no-store by design so new builds land immediately. Do not add an HTTP/CDN cache layer that caches .html. Static JS (icons.js) is cache-busted with a ?v= query, currently ?v=4.
  • Node ≥ 22.5 (the image uses node:24-alpine) — required for the built-in node:sqlite that db.js relies on. deploy.sh rebuilds the image, so npm install (incl. web-push) happens automatically; no manual install.
  • No DB migration is required for routine UI/chat releases. The data.db volume persists across rebuilds; schema changes (when present) auto-apply on boot.

Env vars to confirm in .env

.env lives only on the server (never in git) and must contain, beyond the TURN secrets already documented:

Group Vars Needed for
Login / SSO BIZGAZE_LOGIN_URL, BIZGAZE_DIRECTORY_URL, BIZGAZE_DIRECTORY_TOKEN, SSO_SECRET BizGaze sign-in + directory search
Web Push VAPID_PUBLIC_KEY, VAPID_PRIVATE_KEY, VAPID_SUBJECT Background push for browsers / installed PWA
Native push — Android FCM_SERVICE_ACCOUNT (path to / inline Firebase service-account JSON) FCM push to the Android app
Native push — iOS APNS_KEY (path/inline .p8), APNS_KEY_ID, APNS_TEAM_ID, APNS_BUNDLE_ID, APNS_PRODUCTION=1 APNs push to the iOS app
Calls TURN_URLS / TURN_SECRET (or TURN_USERNAME+TURN_CREDENTIAL) Audio/video across NATs & mobile networks

If the VAPID keys are missing, push silently no-ops (the app still runs). Push on iOS additionally requires the user to Add to Home Screen (iOS 16.4+) — an end-user step, not ops.

Per-release verification

After deploy, open the app and check the browser console logs the expected build, e.g. Biz Connect build 2026-06-30-batch14. That confirms the new HTML is being served (not a stale cache).


One-time bootstrap (server → git clone)

Run once to convert the existing folder into a git checkout without losing the secrets. Prerequisite: the deployment files (Dockerfile, docker-compose.yml, deploy.sh, etc.) are committed and pushed to origin/master first.

ssh -p 61 root@118.95.33.89          # or: plink -ssh -P 61 -pw '<pw>' root@118.95.33.89

# 1. Store git credentials so pulls are non-interactive (once per server).
git config --global credential.helper store
printf 'https://devops%%40bizgaze.com:Bizgaze%%40123@code.bizgaze.com\n' > ~/.git-credentials
chmod 600 ~/.git-credentials

# 2. Stash the live secrets.
cd /opt
cp -a bizgaze-support /opt/bizgaze-support.preclone.bak
mkdir -p /tmp/bzsecrets
cp bizgaze-support/.env                /tmp/bzsecrets/ 2>/dev/null || true
cp bizgaze-support/server/cert.pem     /tmp/bzsecrets/ 2>/dev/null || true
cp bizgaze-support/server/key.pem      /tmp/bzsecrets/ 2>/dev/null || true

# 3. Replace the folder with a fresh clone.
rm -rf bizgaze-support
git clone https://code.bizgaze.com/Sravan/BizGaze_Remote.git bizgaze-support
cd bizgaze-support

# 4. Restore the secrets the clone doesn't carry.
cp /tmp/bzsecrets/.env       ./.env
cp /tmp/bzsecrets/cert.pem   ./server/cert.pem 2>/dev/null || true
cp /tmp/bzsecrets/key.pem    ./server/key.pem  2>/dev/null || true
rm -rf /tmp/bzsecrets

# 5. Build & launch.
chmod +x deploy.sh
docker compose up -d --build
docker compose ps

If .env did not exist yet, create it from the template and fill in the TURN secret:

cp .env.example .env && nano .env

Routine deploy

After pushing changes to origin/master:

ssh -p 61 root@118.95.33.89 'cd /opt/bizgaze-support && ./deploy.sh'

deploy.sh snapshots the current tree (keeping 3 backups), git reset --hards to origin/master, rebuilds, and verifies /api/ice. Flags:

  • ./deploy.sh --no-pull — rebuild the current checkout without pulling
  • ./deploy.sh --rollback — restore the newest backup snapshot and rebuild

Desktop app releases (make the installer live + auto-update)

Web/UI changes reach the desktop app instantly (it loads the live site). Only a change to the native shell (desktop/) needs a new installer. Publishing one both powers the site's "Download for Windows" button and pushes an auto-update to already-installed apps.

The installer feed is served from DOWNLOADS_DIR, which docker-compose now points at /data/downloads (the persistent volume) so uploads survive deploy.sh rebuilds. First time only, redeploy once after pulling so the container picks up DOWNLOADS_DIR, then create the dir:

ssh -p 61 root@118.95.33.89 'docker exec bizgaze-support mkdir -p /data/downloads'

Each desktop release:

  1. Build on a Windows machine (needs desktop/build/icon.ico; bump desktop/package.json version first):
    cd desktop && npm install && npm run dist
    
    Output in desktop/dist/: Biz Connect Setup <ver>.exe, …​.exe.blockmap, latest.yml.
  2. Upload those three files into the container's /data/downloads (all three are required — latest.yml is the update manifest, .blockmap enables differential updates). /data is a named Docker volume (bizgaze_support_data), not a host bind-mount, so its real host path is /var/lib/docker/volumes/bizgaze_support_data/_data. scp straight into it — one step, no restart needed (the server serves the folder live):
    # from the repo's desktop\dist folder on the Windows build machine
    scp -P 61 "Biz Connect Setup <ver>.exe" "Biz Connect Setup <ver>.exe.blockmap" latest.yml `
      root@118.95.33.89:/var/lib/docker/volumes/bizgaze_support_data/_data/downloads/
    
    Alternatively, scp to /tmp on the server and docker cp in (avoids touching the volume path):
    docker cp "/tmp/Biz Connect Setup <ver>.exe"          bizgaze-support:/data/downloads/
    docker cp "/tmp/Biz Connect Setup <ver>.exe.blockmap" bizgaze-support:/data/downloads/
    docker cp "/tmp/latest.yml"                           bizgaze-support:/data/downloads/
    
    Keep both versions' .exe/.blockmap on the server (older blockmaps let installed apps pull deltas); only latest.yml is overwritten — there must be exactly one, pointing at the newest.
  3. Verify:
    curl -I https://remote.bizgaze.com/download/windows      # 302 → the new .exe
    curl    https://remote.bizgaze.com/downloads/latest.yml  # shows version <ver>
    

Installed apps check the feed on launch and every 6h, download in the background, and update on next restart. Keep the .exe + .blockmap that latest.yml references on the server; older versions can be pruned. Note: the installer is not code-signed, so Windows SmartScreen shows an "unknown publisher" warning — supply an EV/OV code-signing cert to remove it (see desktop/PACKAGING.md).


Meetings SFU (LiveKit) — optional, scales meetings past ~5 people

By default meetings use a P2P mesh (each person sends video to every other person), which degrades past ~5 participants. Enabling LiveKit routes media through an SFU so each person uploads once — rooms scale to 20-50+. It's fully optional and config-gated: until you set the three LIVEKIT_* vars, the app keeps using the mesh, unchanged. The livekit service is already in docker-compose.yml; these steps turn it on.

1. Generate an API key + secret (any two random strings; keep them secret):

echo "LIVEKIT_API_KEY=$(openssl rand -hex 8)"
echo "LIVEKIT_API_SECRET=$(openssl rand -hex 24)"

Add those two lines to the server's .env, plus the public signaling URL:

LIVEKIT_URL=wss://livekit.bizgaze.com
LIVEKIT_API_KEY=<from above>
LIVEKIT_API_SECRET=<from above>

The app mints join tokens with the secret (server-side only); the same key/secret reach the livekit container via LIVEKIT_KEYS (docker-compose reads them from this same .env).

2. DNS: point livekit.bizgaze.com (A record) at the server — 118.95.33.89.

3. NPM proxy host for the signaling WebSocket (LiveKit media does NOT go through NPM):

  • Domain livekit.bizgaze.comForward to livekit:7880 (scheme http).
  • Websockets Support: ON. Request an SSL cert (Let's Encrypt) + Force SSL.
  • NPM reaches livekit:7880 by container name — both are on nginx_proxy_manager_default.

4. Media ports — NAT port-forward (REQUIRED here). This box sits behind NAT: its only interface is a private 192.168.88.61; the public 118.95.33.89 (DNS) is mapped by an upstream gateway. WebRTC media can't traverse NPM (L7), so the gateway/router must forward the media ports to the box. To keep the ask minimal, LiveKit is configured for one UDP port + one TCP fallback:

Ask whoever controls the network/gateway to forward, from 118.95.33.89192.168.88.61:

Port Proto Purpose
50000 UDP WebRTC media (all participants mux over this one port)
7881 TCP WebRTC-over-TCP fallback (restrictive client networks)

livekit.yaml already pins rtc.node_ip: 118.95.33.89 (auto-detect would pick the wrong outbound IP behind this NAT). The host's local ufw is inactive, so no host-firewall change is needed — the only requirement is the upstream port-forward above. Until it exists, signaling connects but media won't flow (participants see each other's tiles but no video/audio).

5. Deploy (the livekit service is behind a sfu compose profile, so it stays dormant on a normal deploy — start it explicitly):

cd /opt/bizgaze-support && ./deploy.sh                 # rebuilds/starts the app as usual
docker compose --profile sfu up -d                     # additionally starts the livekit container
docker compose ps                                      # expect both bizgaze-support AND bizgaze-livekit "Up"

6. Verify:

curl https://remote.bizgaze.com/api/meetings/config     # expect {"sfu":true,"url":"wss://livekit.bizgaze.com"}
curl -I https://livekit.bizgaze.com                      # 200/426 (WS endpoint reachable via NPM+TLS)
docker logs bizgaze-livekit --tail 30                    # "starting LiveKit server", no key errors

Then start a meeting in the app and confirm 3+ participants see each other. To roll back to mesh, just remove the LIVEKIT_* vars from .env and redeploy — no code change.


Verify

curl https://remote.bizgaze.com/api/ice

Response must list the public STUN entry and a TURN entry at global.relay.metered.ca. If only STUN appears, .env isn't reaching the container — check docker exec bizgaze-support env | grep TURN.


Rollback

cd /opt/bizgaze-support && ./deploy.sh --rollback   # newest snapshot
# or restore a specific snapshot:
ls -1t /opt/bizgaze-support.backups/*.tgz
tar -xzf /opt/bizgaze-support.backups/<stamp>.tgz -C /opt/bizgaze-support && docker compose up -d --build

The data.db volume is never overwritten by a rebuild, so data is retained regardless of code version.