# docker-dev/ — Mac-friendly multi-cluster fleet for v2 development + manual UI exercise. # # Stack (3 separate Akka clusters — all share the single `OtOpcUa` ConfigDb): # sql SQL Server 2022 — hosts the one ConfigDb that all three clusters use # ldap OpenLDAP with the dev users from C:\publish\glauth\auth.md mirrored in # # Main cluster (existing — split-role admin / driver pair on a single Akka mesh): # admin-a OtOpcUa.Host with OTOPCUA_ROLES=admin (seed) # admin-b OtOpcUa.Host with OTOPCUA_ROLES=admin (joins admin-a) # driver-a OtOpcUa.Host with OTOPCUA_ROLES=driver (joins via admin-a) # driver-b OtOpcUa.Host with OTOPCUA_ROLES=driver (joins via admin-a) # # Site A cluster (2-node fused admin+driver): # site-a-1, site-a-2 OTOPCUA_ROLES=admin,driver, seed = site-a-1 # # Site B cluster (2-node fused admin+driver): # site-b-1, site-b-2 OTOPCUA_ROLES=admin,driver, seed = site-b-1 # # traefik PathPrefix → main cluster admin-a/admin-b; Host(`site-a.localhost`) → # site-a-*; Host(`site-b.localhost`) → site-b-*. Add the two site hosts to # your /etc/hosts (or rely on macOS `.localhost` auto-resolution). # # Multi-tenancy: ConfigDb is one schema with a `ServerCluster` table; each Akka cluster # corresponds to a row in it (ClusterId = "MAIN" / "SITE-A" / "SITE-B"), and each node's # `ClusterNode.NodeId` points back at the row that owns it. After first boot, sign in to # any cluster's Admin UI and create the matching ServerCluster + ClusterNode rows via # /clusters and /hosts so the runtime knows what configuration scope applies. # # Akka mesh isolation: same system name "otopcua" + same remoting port 4053 inside each # container's own network namespace, but with disjoint seed-node lists — gossip never # crosses between the three meshes. # # Usage: # docker compose -f docker-dev/docker-compose.yml up -d --build # open http://localhost # main cluster Blazor admin UI # open http://site-a.localhost # site A admin UI # open http://site-b.localhost # site B admin UI # open http://localhost:8080 # Traefik dashboard # # Tear-down: docker compose -f docker-dev/docker-compose.yml down -v name: otopcua-dev services: sql: image: mcr.microsoft.com/mssql/server:2022-latest environment: ACCEPT_EULA: "Y" SA_PASSWORD: "OtOpcUa!Dev123" MSSQL_PID: Developer ports: - "14330:1433" healthcheck: test: ["CMD-SHELL", "/opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P 'OtOpcUa!Dev123' -No -Q 'SELECT 1' || exit 1"] interval: 10s timeout: 5s retries: 20 # ── Cluster seed (one-shot) ──────────────────────────────────────────────── # Waits for SQL + the host containers' EF auto-migration, then INSERTs the # three ServerCluster rows and the six ClusterNode rows that scope each Akka # mesh inside the shared OtOpcUa ConfigDb. Idempotent — re-runs are no-ops. cluster-seed: image: mcr.microsoft.com/mssql-tools:latest depends_on: sql: condition: service_healthy volumes: - ./seed:/seed:ro entrypoint: ["/bin/bash", "/seed/entrypoint.sh"] restart: "no" ldap: image: bitnami/openldap:2.6 environment: LDAP_ROOT: "dc=lmxopcua,dc=local" LDAP_ADMIN_USERNAME: "admin" LDAP_ADMIN_PASSWORD: "ldapadmin" LDAP_USERS: "alice,bob" LDAP_PASSWORDS: "alice123,bob123" LDAP_USER_DC: "ou=FleetAdmin" ports: - "3893:1389" admin-a: &otopcua-host build: context: .. dockerfile: docker-dev/Dockerfile image: otopcua-host:dev depends_on: sql: { condition: service_healthy } environment: OTOPCUA_ROLES: "admin" ASPNETCORE_URLS: "http://+:9000" ConnectionStrings__ConfigDb: "Server=sql,1433;Database=OtOpcUa;User Id=sa;Password=OtOpcUa!Dev123;TrustServerCertificate=True;" Cluster__Hostname: "0.0.0.0" Cluster__Port: "4053" Cluster__PublicHostname: "admin-a" Cluster__SeedNodes__0: "akka.tcp://otopcua@admin-a:4053" Cluster__Roles__0: "admin" Security__Jwt__SigningKey: "docker-dev-signing-key-with-at-least-32-bytes-of-utf8-content-12345" Security__Jwt__Issuer: "otopcua-dev" Security__Jwt__Audience: "otopcua-dev" Authentication__Ldap__Server: "ldap" Authentication__Ldap__Port: "1389" Authentication__Ldap__AllowInsecureLdap: "true" admin-b: <<: *otopcua-host environment: OTOPCUA_ROLES: "admin" ASPNETCORE_URLS: "http://+:9000" ConnectionStrings__ConfigDb: "Server=sql,1433;Database=OtOpcUa;User Id=sa;Password=OtOpcUa!Dev123;TrustServerCertificate=True;" Cluster__Hostname: "0.0.0.0" Cluster__Port: "4053" Cluster__PublicHostname: "admin-b" Cluster__SeedNodes__0: "akka.tcp://otopcua@admin-a:4053" Cluster__Roles__0: "admin" Security__Jwt__SigningKey: "docker-dev-signing-key-with-at-least-32-bytes-of-utf8-content-12345" Security__Jwt__Issuer: "otopcua-dev" Security__Jwt__Audience: "otopcua-dev" Authentication__Ldap__Server: "ldap" Authentication__Ldap__Port: "1389" Authentication__Ldap__AllowInsecureLdap: "true" driver-a: <<: *otopcua-host environment: OTOPCUA_ROLES: "driver" ConnectionStrings__ConfigDb: "Server=sql,1433;Database=OtOpcUa;User Id=sa;Password=OtOpcUa!Dev123;TrustServerCertificate=True;" Cluster__Hostname: "0.0.0.0" Cluster__Port: "4053" Cluster__PublicHostname: "driver-a" Cluster__SeedNodes__0: "akka.tcp://otopcua@admin-a:4053" Cluster__Roles__0: "driver" ports: - "4840:4840" driver-b: <<: *otopcua-host environment: OTOPCUA_ROLES: "driver" ConnectionStrings__ConfigDb: "Server=sql,1433;Database=OtOpcUa;User Id=sa;Password=OtOpcUa!Dev123;TrustServerCertificate=True;" Cluster__Hostname: "0.0.0.0" Cluster__Port: "4053" Cluster__PublicHostname: "driver-b" Cluster__SeedNodes__0: "akka.tcp://otopcua@admin-a:4053" Cluster__Roles__0: "driver" ports: - "4841:4840" # ── Site A cluster (2-node fused admin+driver) ────────────────────────────── # Shares the OtOpcUa ConfigDb with the main + site-b clusters; multi-tenancy is # enforced by ServerCluster.ClusterId rows (configure via /clusters after boot). # Akka isolation comes from the disjoint seed list (seed = site-a-1). site-a-1: <<: *otopcua-host environment: OTOPCUA_ROLES: "admin,driver" ASPNETCORE_URLS: "http://+:9000" ConnectionStrings__ConfigDb: "Server=sql,1433;Database=OtOpcUa;User Id=sa;Password=OtOpcUa!Dev123;TrustServerCertificate=True;" Cluster__Hostname: "0.0.0.0" Cluster__Port: "4053" Cluster__PublicHostname: "site-a-1" Cluster__SeedNodes__0: "akka.tcp://otopcua@site-a-1:4053" Cluster__Roles__0: "admin" Cluster__Roles__1: "driver" Security__Jwt__SigningKey: "docker-dev-signing-key-with-at-least-32-bytes-of-utf8-content-12345" Security__Jwt__Issuer: "otopcua-dev" Security__Jwt__Audience: "otopcua-dev" Authentication__Ldap__Server: "ldap" Authentication__Ldap__Port: "1389" Authentication__Ldap__AllowInsecureLdap: "true" ports: - "4842:4840" site-a-2: <<: *otopcua-host depends_on: sql: { condition: service_healthy } site-a-1: { condition: service_started } environment: OTOPCUA_ROLES: "admin,driver" ASPNETCORE_URLS: "http://+:9000" ConnectionStrings__ConfigDb: "Server=sql,1433;Database=OtOpcUa;User Id=sa;Password=OtOpcUa!Dev123;TrustServerCertificate=True;" Cluster__Hostname: "0.0.0.0" Cluster__Port: "4053" Cluster__PublicHostname: "site-a-2" Cluster__SeedNodes__0: "akka.tcp://otopcua@site-a-1:4053" Cluster__Roles__0: "admin" Cluster__Roles__1: "driver" Security__Jwt__SigningKey: "docker-dev-signing-key-with-at-least-32-bytes-of-utf8-content-12345" Security__Jwt__Issuer: "otopcua-dev" Security__Jwt__Audience: "otopcua-dev" Authentication__Ldap__Server: "ldap" Authentication__Ldap__Port: "1389" Authentication__Ldap__AllowInsecureLdap: "true" ports: - "4843:4840" # ── Site B cluster (2-node fused admin+driver) ────────────────────────────── site-b-1: <<: *otopcua-host environment: OTOPCUA_ROLES: "admin,driver" ASPNETCORE_URLS: "http://+:9000" ConnectionStrings__ConfigDb: "Server=sql,1433;Database=OtOpcUa;User Id=sa;Password=OtOpcUa!Dev123;TrustServerCertificate=True;" Cluster__Hostname: "0.0.0.0" Cluster__Port: "4053" Cluster__PublicHostname: "site-b-1" Cluster__SeedNodes__0: "akka.tcp://otopcua@site-b-1:4053" Cluster__Roles__0: "admin" Cluster__Roles__1: "driver" Security__Jwt__SigningKey: "docker-dev-signing-key-with-at-least-32-bytes-of-utf8-content-12345" Security__Jwt__Issuer: "otopcua-dev" Security__Jwt__Audience: "otopcua-dev" Authentication__Ldap__Server: "ldap" Authentication__Ldap__Port: "1389" Authentication__Ldap__AllowInsecureLdap: "true" ports: - "4844:4840" site-b-2: <<: *otopcua-host depends_on: sql: { condition: service_healthy } site-b-1: { condition: service_started } environment: OTOPCUA_ROLES: "admin,driver" ASPNETCORE_URLS: "http://+:9000" ConnectionStrings__ConfigDb: "Server=sql,1433;Database=OtOpcUa;User Id=sa;Password=OtOpcUa!Dev123;TrustServerCertificate=True;" Cluster__Hostname: "0.0.0.0" Cluster__Port: "4053" Cluster__PublicHostname: "site-b-2" Cluster__SeedNodes__0: "akka.tcp://otopcua@site-b-1:4053" Cluster__Roles__0: "admin" Cluster__Roles__1: "driver" Security__Jwt__SigningKey: "docker-dev-signing-key-with-at-least-32-bytes-of-utf8-content-12345" Security__Jwt__Issuer: "otopcua-dev" Security__Jwt__Audience: "otopcua-dev" Authentication__Ldap__Server: "ldap" Authentication__Ldap__Port: "1389" Authentication__Ldap__AllowInsecureLdap: "true" ports: - "4845:4840" traefik: image: traefik:v3.1 command: - --entrypoints.web.address=:80 - --providers.file.filename=/etc/traefik/dynamic.yml - --providers.file.watch=true - --api.insecure=true ports: - "80:80" - "8080:8080" volumes: - ./traefik-dynamic.yml:/etc/traefik/dynamic.yml:ro depends_on: - admin-a - admin-b - site-a-1 - site-a-2 - site-b-1 - site-b-2