# docker-dev/ — Mac-friendly multi-cluster fleet for v2 development + manual UI exercise. # # Stack (3 separate Akka clusters sharing the same SQL + LDAP): # sql SQL Server 2022 (per-cluster ConfigDb databases) # 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) # ConfigDb: OtOpcUa # # Site A cluster (2-node fused admin+driver — its own ConfigDb + seed): # site-a-1, site-a-2 OTOPCUA_ROLES=admin,driver, seed = site-a-1 # ConfigDb: OtOpcUa_SiteA # # Site B cluster (2-node fused admin+driver — its own ConfigDb + seed): # site-b-1, site-b-2 OTOPCUA_ROLES=admin,driver, seed = site-b-1 # ConfigDb: OtOpcUa_SiteB # # 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). # # Cluster isolation is enforced by disjoint seed-node lists — Akka.Cluster gossip won't # cross between the three meshes even though they share the same system name "otopcua". # # 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 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) ────────────────────────────── # Its own ConfigDb (OtOpcUa_SiteA) + its own seed (site-a-1) → Akka isolation # from the main cluster and from site B. Both nodes carry both roles. site-a-1: <<: *otopcua-host environment: OTOPCUA_ROLES: "admin,driver" ASPNETCORE_URLS: "http://+:9000" ConnectionStrings__ConfigDb: "Server=sql,1433;Database=OtOpcUa_SiteA;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_SiteA;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_SiteB;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_SiteB;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