CLAUDE.md: ScadaBridge row records main @ 17864d1a (records only, code tip 2b9a7c3c) deployed on vd03 and the vq03/vq04 pair; Theme/Auth/LocalDb rows lose their "not yet deployed" caveats (QA pair now runs LocalDb replication, connected); tier note gains the deployed-state line and the winrm access path. deployment.md: §2.1 access rows replaced by the winrm skill (WinRM 5985 + SMB 445; servecli/psbridge retired), §7.1 port map gains 5985/445 and marks 6150 retired, live-state table updated (OtOpcUa active flipped to node-a), new "Deployed builds on QA" paragraph, §9 deployed-state summary. Claude-Session: https://claude.ai/code/session_01RgthVLVU5CJ8SUuEwruTkt
33 KiB
Deployment & Environments — SCADA/OT family
How the sister projects are deployed: environments, hosts, SSH access, Docker/Traefik topology, databases, and the full service/port map. Compiled 2026-06-03 by reading the actual compose/Traefik/SSH files (not docs alone). For the per-service environment variables see the companion
env_vars.md.Updated 2026-08-13: added the QA stack (
wonder-app-vq03/wonder-app-vq04/wonder-sql-vq03— §7.1), established by probing the live boxes. Same pass correctedwonder-app-vd03, which this doc had called "production" and which is the DEV box (see the ⚠️ note in §2.1).Source confidence: container/port/Traefik/DB facts below are read straight from the compose +
traefik/*.ymlfiles. SSH facts are from~/.ssh/config+~/.ssh/known_hosts. Where a fact is referenced in repo docs but not pinned in a config/script on this machine, it's marked (referenced, not scripted in-repo) — don't treat those as automated.
1. Environment inventory
| Environment | Where it runs | What it is | Entry point |
|---|---|---|---|
ScadaBridge docker |
This Mac (Docker Desktop/OrbStack) | Full hub-and-spoke: 2 Central + 3 sites ×2 nodes | http://localhost:9000 |
ScadaBridge docker-env2 |
This Mac | Second isolated cluster: 2 Central + 1 site ×2 nodes | http://localhost:9100 |
ScadaBridge infra |
This Mac | Shared backing services (MSSQL, OPC-UA sims, SMTP, REST, Playwright) — not LDAP (see shared GLAuth below) | n/a (deps) |
OtOpcUa otopcua-dev |
This Mac | 3 independent Akka clusters (MAIN + SITE-A + SITE-B) sharing one ConfigDb | http://localhost:9200 |
| MxAccessGateway | windev (10.100.0.48), Windows |
Windows-native gRPC gateway + per-session x86 worker (no Docker) | http://10.100.0.48:5120 (gRPC) |
| DEV (VD03) | wonder-app-vd03.zmr.zimmer.com |
Single-node, all-in-one: ScadaBridge + OtOpcUa + MxGateway + HistorianGateway + Overview | see docs/operations/ runbooks |
| QA (VQ03/VQ04) | wonder-app-vq03 + wonder-app-vq04 (+ wonder-sql-vq03) |
2-node HA pair — ScadaBridge central+site pairs, OtOpcUa pair; MxGateway/HistorianGateway/Overview on BOTH nodes | http://wonder-app-vq03.zmr.zimmer.com:5320 (Overview) |
The three ScadaBridge stacks share one external Docker network scadabridge-net; the
OtOpcUa otopcua-dev stack runs on its own default network (otopcua-dev_default) and is
network-isolated from ScadaBridge. All local stacks can run simultaneously — host ports do not
collide (see §7).
On this Apple-Silicon Mac, MSSQL runs under amd64 emulation (slow first-ready; the "platform does not match" warning is expected/benign). See scadabridge-local-deploy-gotchas.
2. Hosts & SSH connectivity
2.1 Host inventory
| Host | Address | OS | Role | SSH port |
|---|---|---|---|---|
| This Mac | local | macOS (darwin) | Dev workstation — runs all local Docker stacks | n/a |
| windev | 10.100.0.48 |
Windows | OtOpcUa Windows-service host + MxAccessGateway (gRPC 5120 / dashboard 5130) |
22 |
| fixture host | 10.100.0.35 |
Debian/Linux + Docker | OtOpcUa driver integration-test fixtures + a test SQL Server | 22 |
| VD03 (DEV) | wonder-app-vd03.zmr.zimmer.com |
Windows | DEV all-in-one box: ScadaBridge (single-node) + OtOpcUa + MxGateway + HistorianGateway + Overview | 2222 |
| VQ03 (QA) | wonder-app-vq03.zmr.zimmer.com (10.220.158.2) |
Windows | QA node A — ScadaBridge central-a + site-a, OtOpcUa node-a, plus the singleton services: MxGateway, HistorianGateway, Overview | 2222 |
| VQ04 (QA) | wonder-app-vq04.zmr.zimmer.com (10.220.158.3) |
Windows | QA node B — ScadaBridge central-b + site-b, OtOpcUa node-b, plus (since 2026-08-14) its own MxGateway, HistorianGateway and Overview (cloned from VQ03 incl. API-key stores; Galaxy SQL points at VQ03 — no local SQL Server) | 2222 |
| wonder-sql-vq03 | wonder-sql-vq03.zmr.zimmer.com |
Windows | QA data tier — SQL Server (1433) + AVEVA Historian gRPC TLS (32565); mirrors wonder-sql-vd03 |
(not probed) |
| gitea | gitea.dohertylan.com (10.100.0.228) |
Linux | Git remotes + NuGet feed (/api/packages/dohertj2/nuget) |
22 |
The lab hosts (windev, fixture, gitea) are on the private 10.x network; the wonder hosts are
on the corporate 10.220.15x network — both need the LAN/VPN.
⚠️
vd= DEV,vq= QA. VD03 was labelled "production" throughout this doc until 2026-08-13; it is not. The convention is confirmed by the family's own MES spec (DEVwonder-sql-vd01vs QAwondersqlqa,mes-delmia-integration-api.md§1.1). Note VD03'sASPNETCORE_ENVIRONMENTgenuinely isProduction— that is a .NET config-overlay name and says nothing about the deployment tier; the two axes are orthogonal. No production box has been identified — do not assume either of these is it.
2.2 How to connect (passwordless SSH)
Auth is key-based (passwordless) with ~/.ssh/id_ed25519 (a legacy ~/.ssh/id_rsa exists
as fallback). Only one host alias is defined in ~/.ssh/config:
# ~/.ssh/config (verified)
Include ~/.orbstack/ssh/config # OrbStack local Linux VMs — use `ssh orb` / `orb` CLI
Host windev
HostName 10.100.0.48
User dohertj2
IdentityFile ~/.ssh/id_ed25519
# Port 22 (default)
| Target | Command | Notes |
|---|---|---|
| windev (Win host) | ssh windev |
Configured alias; user dohertj2, key id_ed25519, port 22 |
| fixture host | ssh dohertj2@10.100.0.35 |
In known_hosts; no config alias — pass user explicitly; port 22, key-based |
| VD03 / VQ03 / VQ04 (all wonder boxes) | winrm ps vd03 '<PowerShell>' · winrm smb push vq03 ./x.zip 'E$\psrem-stage\x.zip' |
The supported path since 2026-09-10 (Claude Code skill winrm, ~/.local/bin/winrm): WinRM 5985/NTLM for commands (5986 closed), SMB 445 on the E$ admin share for files, nothing installed on the box, survives reboots. Profiles vd03/vq03/vq04 (FQDN .zmr.zimmer.com); the domain password lives only in the macOS Keychain. No second hop under NTLM (use SQL logins in scripts). First full round: all five apps on all three boxes, 2026-09-10 |
ssh … -p 2222 |
Retired — plain process, dies on reboot; the psbridge skill/binary were removed the same day (its server.exe on :6150 may still be running on the QA boxes but nothing uses it) |
|
| local Linux VMs | ssh orb / orb |
OrbStack-managed |
⚠️ A TCP-open
2222does NOT mean SSH works. On 2026-08-13 all three wonder boxes (VD03, VQ03, VQ04) accepted a TCP connection on2222and then sent no SSH banner — the server must send its identification string first (RFC 4253 §4.2), so a bannerless socket is a listening port with no working SSH service behind it. The visible symptom issshhanging until it is killed, not a refused connection or an auth error. Every session attempt that day failed this way, so no shell access to any wonder box was available and nothing could be deployed. Probe it with a banner grab, never a port scan:exec 3<>/dev/tcp/<host>/2222 && head -c 60 <&3. servecli is known not to survive a reboot; the documented fallback is RDP.
⚠️
~/binis empty on this Mac. OtOpcUa'sCLAUDE.mdmentions anlmxopcua-fixhelper "in~/bin" for controlling the10.100.0.35fixture containers — it is not present here (it's a Windows-side helper). On this machine, drive the fixture host with direct SSH, e.g.ssh dohertj2@10.100.0.35 'docker compose -f /opt/otopcua-<driver>/docker-compose.yml up -d'. Treat the exact remote paths/commands as (referenced, not scripted in-repo) — verify on the host.
3. ScadaBridge deployment
.NET 10 + Akka.NET. One image scadabridge:latest (built by docker/build.sh) backs every node;
role is chosen by SCADABRIDGE_CONFIG (Central|Site) → appsettings.{role}.json. Central is a
2-node Akka cluster (split-brain resolver = keep-oldest); each Site is its own 2-node Akka
cluster reached from Central via ClusterClient.
3.1 docker/ — primary 3-site cluster (network scadabridge-net)
| Service | Container | Host→container ports | Role | Volumes |
|---|---|---|---|---|
| central-a | scadabridge-central-a |
9001:5000 (UI+Inbound API), 9011:8081 (Akka) |
Central | central-node-a/appsettings.Central.json (ro), …/logs |
| central-b | scadabridge-central-b |
9002:5000, 9012:8081 |
Central | central-node-b/… |
| site-a-a | scadabridge-site-a-a |
9021:8082 (Akka), 9023:8083 (gRPC) |
Site | site-a-node-a/{appsettings.Site.json,data,logs} |
| site-a-b | scadabridge-site-a-b |
9022:8082, 9024:8083 |
Site | site-a-node-b/… |
| site-b-a | scadabridge-site-b-a |
9031:8082, 9033:8083 |
Site | site-b-node-a/… |
| site-b-b | scadabridge-site-b-b |
9032:8082, 9034:8083 |
Site | site-b-node-b/… |
| site-c-a | scadabridge-site-c-a |
9041:8082, 9043:8083 |
Site | site-c-node-a/… |
| site-c-b | scadabridge-site-c-b |
9042:8082, 9044:8083 |
Site | site-c-node-b/… |
| traefik | scadabridge-traefik |
9000:80 (Central LB), 8180:8080 (dashboard) |
LB | traefik/{traefik,dynamic}.yml (ro) |
All restart: unless-stopped; image scadabridge:latest (traefik traefik:v3.4).
Access: Central UI/API via LB http://localhost:9000; direct nodes :9001/:9002; Traefik
dashboard http://localhost:8180; Management API http://localhost:9000/management; health
…/health/ready + …/health/active.
3.2 docker-env2/ — secondary 1-site cluster (same scadabridge-net)
| Service | Container | Host→container ports | Role |
|---|---|---|---|
| central-a | scadabridge-env2-central-a |
9101:5000, 9111:8081 |
Central |
| central-b | scadabridge-env2-central-b |
9102:5000, 9112:8081 |
Central |
| site-x-a | scadabridge-env2-site-x-a |
9121:8082, 9123:8083 |
Site |
| site-x-b | scadabridge-env2-site-x-b |
9122:8082, 9124:8083 |
Site |
| traefik | scadabridge-env2-traefik |
9100:80 (LB), 8181:8080 (dashboard) |
LB |
Access: LB http://localhost:9100; direct :9101/:9102; dashboard http://localhost:8181.
This cluster's DBs and auth cookie name are distinct from docker/ so the two can run on
localhost at once — cookie ZB.MOM.WW.ScadaBridge.Auth.env2 vs the default; see
scadabridge-local-deploy-gotchas.
3.3 infra/ — shared backing services (network scadabridge-net)
| Service | Container | Image | Host ports | Purpose |
|---|---|---|---|---|
| mssql | scadabridge-mssql |
mcr.microsoft.com/mssql/server:2022-latest |
1433:1433 |
SQL Server — Central DBs for both clusters; named vol scadabridge-mssql-data; init via /docker-entrypoint-initdb.d/{setup,machinedata_seed,setup-env2}.sql |
| opcua | scadabridge-opcua |
mcr.microsoft.com/iotedge/opc-plc:latest |
50000:50000, 8080:8080 |
OPC-UA simulator 1 (--unsecuretransport --autoaccept) |
| opcua2 | scadabridge-opcua2 |
…/opc-plc:latest |
50010:50010, 8081:8080 |
OPC-UA simulator 2 |
| smtp | scadabridge-smtp |
axllent/mailpit:latest |
1025:1025, 8025:8025 |
SMTP sink + web UI (http://localhost:8025) |
| restapi | scadabridge-restapi |
local build ./restapi |
5200:5200 |
Test REST endpoint |
| playwright | scadabridge-playwright |
mcr.microsoft.com/playwright:v1.58.2-noble |
3000:3000 |
Browser-automation server |
LDAP is NOT started by
infra/. The per-appscadabridge-ldapcontainer has been retired (commented out ininfra/docker-compose.yml). All three apps (ScadaBridge, OtOpcUa, MxAccessGateway) now share a singlezb-shared-glauthcontainer on the Linux fixture host10.100.0.35:3893(baseDN dc=zb,dc=local, Transport=None). Source of truth and deploy/verify runbook:scadaproj/infra/glauth/(config.toml+docker-compose.yml+README.md); deploy by scp-ing those two files to10.100.0.35and runningdocker compose up -d.
3.4 Traefik (ScadaBridge)
Both clusters use a file provider + insecure API dashboard. traefik.yml: entrypoint web:80,
api.dashboard: true / insecure: true, file provider dynamic.yml. dynamic.yml router
central (PathPrefix(/) → service central) load-balances the two Central containers with an
active health check on /health/active (interval 5s, timeout 3s) — so traffic only routes to
the active leader (standby returns 503 and is dropped from rotation):
# docker/traefik/dynamic.yml (env2 points at scadabridge-env2-central-a/-b)
http:
routers: { central: { rule: "PathPrefix(`/`)", service: central, entryPoints: [web] } }
services:
central:
loadBalancer:
healthCheck: { path: /health/active, interval: 5s, timeout: 3s }
servers: [ {url: "http://scadabridge-central-a:5000"}, {url: "http://scadabridge-central-b:5000"} ]
3.5 Databases (ScadaBridge)
- Central → MSSQL (
scadabridge-mssql:1433), app loginscadabridge_app/ScadaBridge_Dev1#🔒(dev-only):docker/:ScadaBridgeConfig+ScadaBridgeMachineDatadocker-env2/:ScadaBridgeConfig2+ScadaBridgeMachineData2- Created by
infra/mssql/setup.sql+setup-env2.sqlat MSSQL init; EF Core migrations run on Central startup;docker-env2/init-db.shensures the env2 DBs before deploy;seed-sites.shseeds Site rows post-deploy.
- Site → SQLite, per node under the mounted
…/datavolume (SiteDbPath, plus a store-and-forward DB). Not networked, not replicated across hosts.
3.6 Deploy commands (ScadaBridge)
cd ~/Desktop/ScadaBridge
cd infra && docker compose up -d # 1) backing services (MSSQL, OPC-UA, SMTP, REST) — LDAP is shared glauth on 10.100.0.35 (scadaproj/infra/glauth/)
bash docker/build.sh # 2) create scadabridge-net (if missing) + build scadabridge:latest
bash docker/deploy.sh # 3) up -d --force-recreate; prints access points (9000/9001/9002/8180)
bash docker/seed-sites.sh # 4) seed sites + data-connections (optional)
# env2 cluster:
bash docker-env2/deploy.sh # reuses the image; runs init-db.sh; ports 9100/9101/9102/8181
Caveat:
deploy.shdoesup -d --force-recreate, starting both Central nodes at once — they can split-brain on a simultaneous start. Start Central sequenced (central-a → wait/health/active200 → central-b). Central also requiresScadaBridge__InboundApi__ApiKeyPepper(dev value is inline in both composes). Full detail: scadabridge-local-deploy-gotchas.
4. OtOpcUa deployment (otopcua-dev)
.NET 10 OPC-UA server. Three independent Akka clusters share the single OtOpcUa ConfigDb
(multi-tenancy via the ServerCluster table); Akka isolation is by disjoint seed lists (same
system name otopcua, internal remoting port 4053). Built locally from docker-dev/Dockerfile
→ image otopcua-host:dev. No per-app LDAP container — docker-dev is un-stubbed
(Authentication__Ldap__DevStubMode removed) and binds the shared GLAuth at
10.100.0.35:3893 (baseDN dc=zb,dc=local, Transport=None). Start the shared glauth first via
scadaproj/infra/glauth/ if it is not already running.
| Service | Container | Host→container ports | Cluster / role |
|---|---|---|---|
| sql | (otopcua-dev-sql-1) |
14330:1433 |
SQL Server 2022 — the shared OtOpcUa ConfigDb |
| cluster-seed | one-shot | — | mssql-tools running /seed/entrypoint.sh (idempotent ServerCluster/ClusterNode seed) |
| admin-a | host | (none — internal :9000 UI behind Traefik) |
MAIN, role admin (seed) |
| admin-b | host | (none) | MAIN, role admin (joins admin-a) |
| driver-a | host | 4840:4840 (OPC UA) |
MAIN, role driver |
| driver-b | host | 4841:4840 |
MAIN, role driver |
| site-a-1 | host | 4842:4840 |
SITE-A, admin,driver (seed) |
| site-a-2 | host | 4843:4840 |
SITE-A, admin,driver |
| site-b-1 | host | 4844:4840 |
SITE-B, admin,driver (seed) |
| site-b-2 | host | 4845:4840 |
SITE-B, admin,driver |
| traefik | host | 9200:80 (Admin UI LB), 8089:8080 (dashboard) |
traefik:v3.1 |
- OPC UA endpoints:
opc.tcp://localhost:4840(driver-a) …:4845(site-b-2). Admin nodes serve no OPC UA. - Admin UI (Traefik, sticky cookie
otopcua_lb, health-checked on/health/active):- MAIN cluster:
http://localhost:9200 - SITE-A:
http://site-a.localhost:9200· SITE-B:http://site-b.localhost:9200(Host-header routing; macOS auto-resolves*.localhost) - Traefik dashboard:
http://localhost:8089
- MAIN cluster:
- DB:
sqlservice,14330:1433, SAOtOpcUa!Dev123🔒(dev-only), databaseOtOpcUa; EF auto-migrates on host start, thencluster-seedinserts the 3 ServerCluster + 6 ClusterNode rows. - Deploy:
docker compose -f docker-dev/docker-compose.yml up -d --build; tear down with… down -v. - Galaxy link: driver nodes resolve
GALAXY_MXGW_API_KEYand connect out to MxAccessGateway (see §5).
Integration-test fixtures (separate from this stack) run on the Linux fixture host
10.100.0.35(Modbus:5020, Allen-Bradley:44818, S7:102, OPC-UA:50000, SQL:14330). Those are test endpoints, not the deployed app; per-fixture env defaults are inenv_vars.md§1.3.
5. MxAccessGateway deployment (Windows-native, no Docker)
Two processes: an x64 .NET 10 Server (ASP.NET Core gRPC + Blazor dashboard) and a per-session
x86 .NET 4.8 Worker that owns the 32-bit AVEVA MXAccess COM/STA. Windows-only. Deployed on
windev (10.100.0.48) and VD03, run as a Windows Service via NSSM (config delivered as
Kestrel__Endpoints__… environment variables, not appsettings.json).
5.1 Endpoint/port map
| Endpoint | Default URL | Protocol | Config key | Purpose |
|---|---|---|---|---|
| Http (gRPC) | http://0.0.0.0:5120 (h2c) |
HTTP/2 cleartext | Kestrel__Endpoints__Http__Url / __Protocols=Http2 |
Public gRPC: sessions, MxCommand/MxEvent, Galaxy browse |
| Dashboard | http://0.0.0.0:5130 |
HTTP/1.1 | Kestrel__Endpoints__Dashboard__Url |
Blazor dashboard + SignalR hubs + /login |
Local dev (launchSettings.json): gRPC http://localhost:5120 (https dev profile adds 7121).
TLS optional — set …Http__Url=https://…; the gateway auto-generates a self-signed cert if none is
supplied (docs/GatewayConfiguration.md). Dashboard cookie name is now configurable
(MxGateway:Dashboard:CookieName).
5.2 Run / host
# local dev
dotnet run --project src/ZB.MOM.WW.MxGateway.Server/ZB.MOM.WW.MxGateway.Server.csproj
# the x86 worker must be published first; path = MxGateway:Worker:ExecutablePath
dotnet build src/MxGateway.Worker/MxGateway.Worker.csproj -p:Platform=x86
- Worker model: the Server spawns one
ZB.MOM.WW.MxGateway.Worker.exe(x86) per gRPC session; IPC over a named pipe (\\.\pipe\mxgateway-<session>+ a per-sessionMXGATEWAY_WORKER_NONCE); heartbeat 5s / grace 15s; max 64 concurrent sessions. The worker exits when the session closes. - Deployed hosts:
10.100.0.48(windev),wonder-app-vd03(DEV),wonder-app-vq03and (since 2026-08-14)wonder-app-vq04(QA) all serve gRPC on:5120(perdocs/GatewayConfiguration.md). QA runs MxGateway on BOTH nodes — VQ04's was cloned from VQ03 (same API-key store + pepper, so the same tokens verify on either; its Galaxy SQL and its alarm subscription\\wonder-app-vq03\Galaxy!NAMboth point at VQ03).
5.3 Who connects to it
| Client | Connects to | Auth |
|---|---|---|
OtOpcUa GalaxyDriver |
http://10.100.0.48:5120 (gRPC) |
API key via GALAXY_MXGW_API_KEY (mxgw_… bearer) 🔒 |
| ScadaBridge MxGateway adapter | same gRPC endpoint :5120 |
API key |
6. Cross-project runtime data flow (deployed)
AVEVA Galaxy (Wonderware) ──MXAccess COM (32-bit)──► MxAccessGateway (windev:5120 gRPC / :5130 dashboard)
▲ ▲
OtOpcUa GalaxyDriver ───gRPC────┘ │ gRPC
(otopcua-dev: opc.tcp :4840–4845) │
│ OPC UA │
▼ │
ScadaBridge DCL ◄──OPC UA──┐ ┌──MxGateway adapter──┘
(docker :9000 / env2 :9100) └───┘
ScadaBridge reaches Wonderware data two ways: (1) OPC UA → OtOpcUa → gateway, or (2) its
MxGateway adapter → gateway directly. The break surface is the wire contracts (the gateway .protos
and OtOpcUa's OPC-UA address space), not compile references.
7. Consolidated host port map
Every published host port across the local stacks (no collisions — all can run at once):
| Port | → Container:port | Service | Stack |
|---|---|---|---|
| 1025 | scadabridge-smtp:1025 |
SMTP submission | infra |
| 1433 | scadabridge-mssql:1433 |
SQL Server (ScadaBridge Central DBs) | infra |
| 3000 | scadabridge-playwright:3000 |
Playwright server | infra |
| 3893 | zb-shared-glauth:3893 on 10.100.0.35 |
LDAP (shared GLAuth — remote fixture host, not a local container) | scadaproj/infra/glauth/ |
| 5200 | scadabridge-restapi:5200 |
Test REST API | infra |
| 8025 | scadabridge-smtp:8025 |
Mailpit web UI | infra |
| 8080 | scadabridge-opcua:8080 |
OPC-UA sim 1 web UI | infra |
| 8081 | scadabridge-opcua2:8080 |
OPC-UA sim 2 web UI | infra |
| 50000 | scadabridge-opcua:50000 |
OPC-UA sim 1 endpoint | infra |
| 50010 | scadabridge-opcua2:50010 |
OPC-UA sim 2 endpoint | infra |
| 9000 | scadabridge-traefik:80 |
Central UI/API (LB) | docker |
| 8180 | scadabridge-traefik:8080 |
Traefik dashboard | docker |
| 9001 / 9002 | central-a / central-b :5000 | Central UI+Inbound API (direct) | docker |
| 9011 / 9012 | central-a / central-b :8081 | Akka remoting | docker |
| 9021–9024 | site-a-a/b :8082 / :8083 | Site A Akka / gRPC | docker |
| 9031–9034 | site-b-a/b :8082 / :8083 | Site B Akka / gRPC | docker |
| 9041–9044 | site-c-a/b :8082 / :8083 | Site C Akka / gRPC | docker |
| 9100 | scadabridge-env2-traefik:80 |
Central UI/API (LB) | docker-env2 |
| 8181 | scadabridge-env2-traefik:8080 |
Traefik dashboard | docker-env2 |
| 9101 / 9102 | env2 central-a / central-b :5000 | Central (direct) | docker-env2 |
| 9111 / 9112 | env2 central-a / central-b :8081 | Akka remoting | docker-env2 |
| 9121–9124 | env2 site-x-a/b :8082 / :8083 | Site X Akka / gRPC | docker-env2 |
| 14330 | otopcua-dev sql :1433 |
SQL Server (OtOpcUa DB) |
otopcua-dev |
| 4840 / 4841 | driver-a / driver-b :4840 | OPC UA (MAIN) | otopcua-dev |
| 4842 / 4843 | site-a-1 / site-a-2 :4840 | OPC UA (SITE-A) | otopcua-dev |
| 4844 / 4845 | site-b-1 / site-b-2 :4840 | OPC UA (SITE-B) | otopcua-dev |
| 9200 | otopcua-dev traefik :80 |
Admin UI (LB) | otopcua-dev |
| 8089 | otopcua-dev traefik :8080 |
Traefik dashboard | otopcua-dev |
Remote (non-local) endpoints: MxAccessGateway gRPC 10.100.0.48:5120 (h2c) / dashboard :5130;
DEV gRPC on wonder-app-vd03:5120, QA on wonder-app-vq03:5120. SSH: windev/fixture/gitea on 22,
VD03/VQ03/VQ04 on 2222.
7.1 QA port map: VQ03 and VQ04
Established 2026-08-13 from two independent live sources, neither of them a config file, and updated 2026-08-14 when the three singleton services were cloned onto VQ04 (that update IS from the boxes' live NSSM config + health probes, so its rows are verified). Rows are marked with how each was established:
scan— TCP port scan of both hosts, plus a/health/readyGET where one was served.dash— read off the QA Overview dashboard (vq03:5320), which was already polling the QA fleet and reports each instance's endpoint, health entries and active/standby state. Stronger evidence than a scan for these rows: the dashboard is successfully talking to the service, not merely finding an open socket. These ports were never scanned by me.svc/live— added 2026-08-14: read from the boxes' own service registry (NSSMAppEnvironmentExtra/Win32_Service) or exercised end-to-end during the VQ04 singleton setup.
| Port | VQ03 | VQ04 | How | Service |
|---|---|---|---|---|
| 9000 | ✅ | ✅ | dash | OtOpcUa host + AdminUI (same listener) |
| 4840 | ✅ | ✅ | scan | OtOpcUa OPC UA endpoint (opc.tcp) |
| 4053 | ✅ | ✅ | dash | OtOpcUa Akka remoting (from the reported leader address) |
| 8085 | ✅ | ✅ | scan | ScadaBridge Central — CentralUI + Management + Inbound API |
| 8084 | ✅ | ✅ | dash | ScadaBridge Site health listener (no UI; / → 404) |
| 8081 | ✅ | ✅ | scan | ScadaBridge Central Akka remoting |
| 8082 / 8083 | ✅ | ✅ | scan | ScadaBridge Site Akka / gRPC control plane (h2c — rejects an HTTP/1.1 GET) |
| 5120 / 5130 | ✅ | ✅ | scan | MxAccessGateway gRPC / dashboard (VQ04 added 2026-08-14, cloned from VQ03) |
| 5220 / 5221 | ✅ | ✅ | scan | HistorianGateway dashboard / gRPC h2c (VQ04 added 2026-08-14) |
| 5320 | ✅ | ✅ | scan | Overview dashboard (one per node since 2026-08-14; both watch the same 10-instance registry) |
| 1433 | ✅ | ✗ | scan | SQL Server (ZB Galaxy repo DB — VQ03 only; VQ04's gateways point their Galaxy connection strings here) |
| 3893 | ✅ | ✅ | svc | glauth LDAP (ScadaBridge-LDAP service, loopback-only, dc=scadabridge,dc=local — one per box, configs identical) |
| 5985 | ✅ | ✅ | live | WinRM (HTTP/NTLM; 5986 refused) — the supported access path since 2026-09-10 via the winrm skill; see §2.1 |
| 445 | ✅ | ✅ | live | SMB — E$/C$ admin shares for file transfer (winrm smb push/pull) |
| 6150 | ⚠️ | ⚠️ | live | psbridge server.exe (root jail E:\psrem) — retired 2026-09-10, superseded by WinRM; may still be listening |
| 2222 | ⚠️ | ⚠️ | scan | servecli SSH — retired; TCP-open but NOT serving SSH (see below) |
Live state at the 2026-08-13 capture (8 instances across 4 applications, all Up; since
2026-08-14 the registry is 10 instances — MxAccessGateway and HistorianGateway run on both
nodes, verified Healthy at setup):
| Application | VQ03 | VQ04 |
|---|---|---|
OtOpcUa (cluster MAIN) |
node-a — Active since the 2026-09-10 pair restart (was Standby at the 08-13 capture) | node-b — Standby (was Active) |
ScadaBridge central |
central-a — Active | central-b — Standby |
ScadaBridge site |
site-a — Active | site-b — Standby |
| MxAccessGateway | Active | Active (since 2026-08-14; independent gateways, no cluster) |
| HistorianGateway | Up | Up (since 2026-08-14) |
| Overview | Up (:5320) |
Up (:5320, since 2026-08-14; same 10-instance registry) |
Deployed builds on QA (2026-09-10 round, all via the winrm skill, both nodes): ScadaBridge
2b9a7c3c (all four nodes stopped together, one shared-DB migration 53→55, 2 min 35 s window,
rollback = F:\MSSQL\BACKUP\ScadaBridge_pre-2b9a7c3c_20260910T121009.bak on wonder-sql-vq03 because
the old binary cannot read the now-encrypted protocol-config row), OtOpcUa b8b4c18f (pair stopped
together, 72 s; LocalDb replication connected afterwards for the first time; QA's config DB holds no
deployment, so the address space is empty), MxAccessGateway 4f931e4 Server + 4f931e4b x86 Worker
(rolling, one stop per box), HistorianGateway d8d890c (rolling), Overview 818775b3 (code-identical to
846b77f, not redeployed). Every box keeps app.bak.20260910T* next to app\, staging under
E:\psrem-stage\<app>-<sha>\, and a _DEPLOY-NOTES-20260910T*.txt per app. Login is enabled on QA
(unlike DEV) and was preserved; every appsettings file was restored hash-identical and no service
environment was edited. ScadaBridge records: ScadaBridge/docs/deployment/deployment-records/2026-09-10-*.md.
The 2026-08-14 VQ04 clone, in one paragraph: MxAccessGw + HistorianGateway + ZbOverview were
copied from VQ03 (same E:\ApiInstall\... layout, NSSM AppEnvironmentExtra config, LocalSystem)
including the API-key stores and peppers, so the same mxgw_…/histgw_… tokens verify on both
boxes — proven with positive + negative controls. The only config deltas: both apps' Galaxy SQL
connection strings point at VQ03 (Server=wonder-app-vq03…;Database=ZB — VQ04 has no local SQL
Server) and mxgw's alarm subscription stays \\wonder-app-vq03\Galaxy!NAM. So VQ04's gateways
depend on VQ03 for Galaxy browse + alarms; Historian/Runtime-DB traffic goes to wonder-sql-vq03
independently. Deploy record: E:\ApiInstall\_DEPLOY-NOTES-20260814-singletons-added.txt on VQ04.
Three things worth carrying forward:
- QA is a real 2-node HA pair; DEV (VD03) is a single box. So QA is the only environment here where failover, leader election and the active/standby split are actually exercised. A change that works on VD03 has not been tested against a pair.
- The active node is not the same for every application. At capture, ScadaBridge's central and site singletons were on VQ03 while OtOpcUa's active node was VQ04 — these are independent Akka clusters and there is no reason for them to agree. Don't infer one from the other.
- QA's Overview registry can use real FQDNs, unlike VD03's.
LeaderResolvermatches a reported Akka leader to an instance by host only, which needs distinct hosts per instance — true on QA, false on the co-located VD03 box, where the workaround is to point everyBaseUrlat127.0.0.1so resolution never matches and each chip falls back to the raw Akka address. On QA the leader chips resolve to real names (node-a (vq03)), so that workaround is not wanted here.
8. Secrets & dev-only values
Every credential shown above (OtOpcUa!Dev123, ScadaBridge_Dev1#, the inline API-key peppers,
the docker-dev JWT signing key, the mxgw_… API key) is a dev-only placeholder for the local
stacks — never reuse as a real secret. The deployed wonder boxes (DEV + QA) inject real secrets
out-of-band (NSSM env / secret store), per ScadaBridge
docs/operations/inbound-api-key-reissue.md. Secrets are per-stack — a DEV value is never a QA
value, and inbound API keys in particular cannot be copied between them (§9). The full
🔒 secret inventory and the __-env-var override forms are in env_vars.md §5.
9. Wonder boxes: DEV and QA
wonder-app-vd03.zmr.zimmer.com (reach it with the winrm skill, §2.1) is the DEV all-in-one box — single-node
ScadaBridge plus OtOpcUa, MxGateway (gRPC :5120), HistorianGateway and Overview.
wonder-app-vq03 + wonder-app-vq04 are the QA pair (§7.1).
Both tiers run every repo tip since 2026-09-10 — DEV upgraded 10:45–11:20 (records per app on the
box under E:\ApiInstall\<App>\_DEPLOY-NOTES-20260910T*.txt; ScadaBridge record in its repo), QA
11:48–12:22 (§7.1 "Deployed builds on QA"). DEV runs mxgw's Worker rebuilt on windev (the x86 net48
Worker cannot be built on macOS; ssh windev is the build box for it).
Neither install is a scripted in-repo flow — the operational procedures live in ScadaBridge
docs/operations/ (failover-procedures.md, maintenance-procedures.md,
inbound-api-key-reissue.md, troubleshooting-guide.md). Treat any service/port specifics not in
those runbooks, or not probed in §7.1, as unverified.
Auth posture differs between the two, and QA is the correct one:
| VD03 (DEV) | VQ03/VQ04 (QA) | |
|---|---|---|
POST :8085/management |
No auth (DisableLogin=true) — ListApiKeys/CreateApiKey succeed unauthenticated from off-box |
Basic auth required (AUTH_FAILED without it) |
That is expected on a dev box and is not a finding there, but it means any DEV-only procedure that relies on unauthenticated management calls will not work on QA — supply credentials.
Inbound API keys do not cross environments. Each stack mints its own secrets (peppered-HMAC,
never recoverable) and config bundles deliberately carry no keys, so a QA sbk_… token 401s on DEV
and its keyId is absent from DEV's store entirely — verified 2026-08-13. Probe recipe: POST :8085/api/MesMoveIn -d '{}' → 400 VALIDATION_FAILED means the key is valid on that stack
(it reached the method and executed nothing); 401 means rejected. Pair it with a positive control
— auth runs before route resolution, so a bad method name also returns 401.