Files
lmxopcua/docker-dev/README.md
Joseph Doherty 7e3b56c27d feat(deploy): Traefik active-leader routing + docker-dev compose (Task 63)
- scripts/install/traefik.yml + traefik-dynamic.yml: Traefik static + dynamic
  config. One :80 entry point, one router on HostRegexp(otopcua.*), one
  service load-balancing admin-a:9000 + admin-b:9000 with /health/active health
  check (interval 5s, timeout 2s, expected 200). Followers return 503 from
  /health/active so Traefik drops them within the next interval after a
  leadership change.

- scripts/install/Install-Traefik.ps1: downloads Traefik for Windows, drops the
  yml configs, registers the OtOpcUaTraefik Windows service via sc.exe with
  restart-on-failure. Companion to Install-Services.ps1.

- docker-dev/{Dockerfile,docker-compose.yml,traefik-dynamic.yml,README.md}:
  Mac-friendly four-node fleet (admin-a + admin-b + driver-a + driver-b) plus
  SQL Server 2022 + OpenLDAP + Traefik. Single OtOpcUa.Host image built once;
  Compose drives OTOPCUA_ROLES + Cluster:* per container to differentiate the
  four hosts. README walks through bring-up + failover smoke + the dev LDAP
  users.

Note: untested on macOS (no local Docker — see docs/v2/dev-environment.md).
2026-05-26 06:46:40 -04:00

3.2 KiB

docker-dev

Mac-friendly four-node OtOpcUa fleet for manual UI exercise + integration smoke tests. Spins up an Akka cluster + SQL Server + OpenLDAP + Traefik in front of two admin nodes.

Stack

Service Role Ports
sql SQL Server 2022 (ConfigDb backing store) host 14330 → container 1433
ldap OpenLDAP with dev users alice / bob host 3893 → container 1389
admin-a OtOpcUa.Host, OTOPCUA_ROLES=admin, cluster seed internal 9000
admin-b OtOpcUa.Host, OTOPCUA_ROLES=admin, joins admin-a internal 9000
driver-a OtOpcUa.Host, OTOPCUA_ROLES=driver host 4840 → container 4840
driver-b OtOpcUa.Host, OTOPCUA_ROLES=driver host 4841 → container 4840
traefik Routes :80 to whichever admin-* currently passes /health/active host 80, dashboard 8080

All six containers share an Akka cluster bound to port 4053 inside the Compose network. The Akka PublicHostname of each container matches its Compose service name; the seed-node list points at admin-a so the other three join via that.

Bring up

# from the repo root
docker compose -f docker-dev/docker-compose.yml up -d --build

# wait ~15 seconds for SQL to come up + the cluster to form

open http://localhost          # Blazor admin UI via Traefik
open http://localhost:8080     # Traefik dashboard

The first build takes a few minutes (.NET SDK image + restore + publish). Subsequent rebuilds are faster with Docker's layer cache.

Auth (dev only)

Use one of the LDAP dev users from LDAP_USERS in docker-compose.yml:

Username Password
alice alice123
bob bob123

The compose mounts everyone into ou=FleetAdmin so the dev role mapping resolves to FleetAdmin.

Tear down

docker compose -f docker-dev/docker-compose.yml down -v

The -v drops the SQL + LDAP volumes; remove it to keep ConfigDb state across restarts.

Failover smoke

  1. Watch the Traefik dashboard at http://localhost:8080. Both admin-a and admin-b should be listed as healthy in the otopcua-admin service.
  2. docker compose -f docker-dev/docker-compose.yml stop admin-aadmin-b should pick up the admin role-leader within ~15 s (Akka split-brain stable-after). Traefik will route traffic to admin-b once its /health/active returns 200.
  3. docker compose -f docker-dev/docker-compose.yml start admin-aadmin-a rejoins as a follower; admin-b keeps the leader role until something disturbs it.

Notes

  • This compose is for the local Mac/Linux developer rig. The team's CI + soak runs go to the remote docker host at 10.100.0.35 (see docs/v2/dev-environment.md); the file there mirrors this one with adjusted port bindings.
  • The OPC UA driver endpoints (opc.tcp://localhost:4840, opc.tcp://localhost:4841) are reachable directly from the host — Traefik is only in front of the admin HTTP surface.
  • Galaxy + Wonderware drivers can't run in Linux containers (they need the Windows-only mxaccessgw + Historian SDK). On non-Windows, DriverInstanceActor.ShouldStub(driverType, roles) returns true for those types and the actor goes straight to a Stubbed state that returns deterministic success.