# Secrets 0.6.2 requires Secrets:SqlitePath to be absolute AND outside the content root # (/app) — mirrors docker/docker-compose.yml's x-secrets-store-env anchor, including the # trap documented there: DELETING the key does not fall back to the per-user default, it # yields the RELATIVE "secrets.db", which fails both rules and the node refuses to boot. # (env2 ran a stale pre-0.6.2 image until the 2026-08-18 rebuild, which surfaced this.) # /data is the node's own data volume mounted a second time outside /app, so the store # shares the durable volume without living under the content root. x-secrets-store-env: &secrets-store-env Secrets__SqlitePath: "/data/scadabridge-secrets.db" services: central-a: image: scadabridge:latest # An init process (tini) as PID 1, so a crashed dotnet process actually dies # instead of wedging the container at 100% CPU with the restart policy never # firing — mirrors docker/docker-compose.yml's init: true (ScadaBridge#34). init: true # CoordinatedShutdown needs cluster-leave (15s budget) + cluster-exiting + # actor-system-terminate + Serilog flush; the 10s SIGTERM default SIGKILLed # mid-drain, turning every redeploy into the crash path (review 01 [Medium]). stop_grace_period: 30s container_name: scadabridge-env2-central-a environment: <<: *secrets-store-env SCADABRIDGE_CONFIG: Central ASPNETCORE_ENVIRONMENT: Development ASPNETCORE_URLS: "http://+:5000" # DEV-ONLY local-cluster value — NOT a real secret. The Auth/Config normalization # (2026-06-03) made ScadaBridge:InboundApi:ApiKeyPepper a hard Central startup # requirement (>=16 chars, per-environment). Distinct from the docker/ cluster's # pepper per the "different per environment" guidance; real deployments inject a # true secret out-of-band, never from source control. Both Central nodes share it. ScadaBridge__InboundApi__ApiKeyPepper: "dev-only-insecure-pepper-env2-cluster-0001" # DEV-ONLY local-cluster secrets — NOT real secrets. Relocated out of the mounted # appsettings.Central.json (mirrors docker/docker-compose.yml's G-4 T5 secrets # cleanup) so that app-config file carries no plaintext credentials. Env overrides # layer over JSON BEFORE the ${secret:} pre-host expander runs. The SQL password is # the same value already committed for the sibling scadabridge-mssql container # (infra/docker-compose.yml MSSQL_SA_PASSWORD) and for docker/'s central nodes — # consolidation, not new exposure; env2's Config2/MachineData2 databases live on # that SAME shared mssql container. Real deployments use ${secret:} tokens + # a seeded store. ScadaBridge__Database__ConfigurationDb: "Server=scadabridge-mssql,1433;Database=ScadaBridgeConfig2;User Id=scadabridge_app;Password=ScadaBridge_Dev1#;TrustServerCertificate=true" ScadaBridge__Database__MachineDataDb: "Server=scadabridge-mssql,1433;Database=ScadaBridgeMachineData2;User Id=scadabridge_app;Password=ScadaBridge_Dev1#;TrustServerCertificate=true" # LDAP bind credential for the redundant local GLAuth pair (scadaproj/infra/glauth-redundant, # host.docker.internal:3893/:3894 — see central-node-a/appsettings.Central.json). Same # value docker/'s central nodes use for the SAME local pair; the shared 10.100.0.35 # GLAuth's serviceaccount password was rotated (SEC-36) and is no longer valid. ScadaBridge__Security__Ldap__ServiceAccountPassword: "serviceaccount123" # DEV-ONLY local-cluster secret — NOT a real secret. Distinct from docker/'s JWT key # so sessions cannot cross environments (see docker-env2/README.md). ScadaBridge__Security__JwtSigningKey: "scadabridge-env2-dev-jwt-signing-key-must-be-at-least-32-characters-long" # DEV-ONLY gRPC control-plane preshared key for site-x — NOT a real secret. # Must match ScadaBridge:Communication:GrpcPsk in site-x-node-*/appsettings.Site.json. # Production seeds SB-GRPC-PSK- into the secret store instead. ScadaBridge__Communication__SitePsks__site-x: "dev-grpc-psk-docker-env2-site-x" ports: - "9101:5000" # Web UI + Inbound API - "9111:8081" # Akka remoting - "9113:8083" # gRPC control plane (CentralControlService) — mirrors docker/'s 9013 volumes: - ./central-node-a/appsettings.Central.json:/app/appsettings.Central.json:ro - central-node-a-data:/app/data # Same named volume, mounted a second time OUTSIDE the content root (/app) for the # secrets store — see the x-secrets-store-env anchor above. - central-node-a-data:/data - ./central-node-a/logs:/app/logs networks: - scadabridge-net restart: unless-stopped central-b: image: scadabridge:latest init: true # PID-1 crash wedge, ScadaBridge#34 — see central-a # CoordinatedShutdown needs cluster-leave (15s budget) + cluster-exiting + # actor-system-terminate + Serilog flush; the 10s SIGTERM default SIGKILLed # mid-drain, turning every redeploy into the crash path (review 01 [Medium]). stop_grace_period: 30s container_name: scadabridge-env2-central-b environment: <<: *secrets-store-env SCADABRIDGE_CONFIG: Central ASPNETCORE_ENVIRONMENT: Development ASPNETCORE_URLS: "http://+:5000" # DEV-ONLY local-cluster value — NOT a real secret. The Auth/Config normalization # (2026-06-03) made ScadaBridge:InboundApi:ApiKeyPepper a hard Central startup # requirement (>=16 chars, per-environment). Distinct from the docker/ cluster's # pepper per the "different per environment" guidance; real deployments inject a # true secret out-of-band, never from source control. Both Central nodes share it. ScadaBridge__InboundApi__ApiKeyPepper: "dev-only-insecure-pepper-env2-cluster-0001" # DEV-ONLY local-cluster secrets — NOT real secrets. Relocated out of the mounted # appsettings.Central.json (mirrors docker/docker-compose.yml's G-4 T5 secrets # cleanup) so that app-config file carries no plaintext credentials. Env overrides # layer over JSON BEFORE the ${secret:} pre-host expander runs. The SQL password is # the same value already committed for the sibling scadabridge-mssql container # (infra/docker-compose.yml MSSQL_SA_PASSWORD) and for docker/'s central nodes — # consolidation, not new exposure; env2's Config2/MachineData2 databases live on # that SAME shared mssql container. Real deployments use ${secret:} tokens + # a seeded store. ScadaBridge__Database__ConfigurationDb: "Server=scadabridge-mssql,1433;Database=ScadaBridgeConfig2;User Id=scadabridge_app;Password=ScadaBridge_Dev1#;TrustServerCertificate=true" ScadaBridge__Database__MachineDataDb: "Server=scadabridge-mssql,1433;Database=ScadaBridgeMachineData2;User Id=scadabridge_app;Password=ScadaBridge_Dev1#;TrustServerCertificate=true" # LDAP bind credential for the redundant local GLAuth pair (scadaproj/infra/glauth-redundant, # host.docker.internal:3893/:3894 — see central-node-b/appsettings.Central.json). Same # value docker/'s central nodes use for the SAME local pair; the shared 10.100.0.35 # GLAuth's serviceaccount password was rotated (SEC-36) and is no longer valid. ScadaBridge__Security__Ldap__ServiceAccountPassword: "serviceaccount123" # DEV-ONLY local-cluster secret — NOT a real secret. Distinct from docker/'s JWT key # so sessions cannot cross environments (see docker-env2/README.md). ScadaBridge__Security__JwtSigningKey: "scadabridge-env2-dev-jwt-signing-key-must-be-at-least-32-characters-long" # DEV-ONLY gRPC control-plane preshared key for site-x — NOT a real secret. # Must match ScadaBridge:Communication:GrpcPsk in site-x-node-*/appsettings.Site.json. # Production seeds SB-GRPC-PSK- into the secret store instead. ScadaBridge__Communication__SitePsks__site-x: "dev-grpc-psk-docker-env2-site-x" ports: - "9102:5000" # Web UI + Inbound API - "9112:8081" # Akka remoting - "9114:8083" # gRPC control plane (CentralControlService) — mirrors docker/'s 9014 volumes: - ./central-node-b/appsettings.Central.json:/app/appsettings.Central.json:ro - central-node-b-data:/app/data # Same named volume, mounted a second time OUTSIDE the content root (/app) for the # secrets store — see the x-secrets-store-env anchor above. - central-node-b-data:/data - ./central-node-b/logs:/app/logs networks: - scadabridge-net restart: unless-stopped site-x-a: image: scadabridge:latest init: true # PID-1 crash wedge, ScadaBridge#34 — see central-a # CoordinatedShutdown needs cluster-leave (15s budget) + cluster-exiting + # actor-system-terminate + Serilog flush; the 10s SIGTERM default SIGKILLed # mid-drain, turning every redeploy into the crash path (review 01 [Medium]). stop_grace_period: 30s container_name: scadabridge-env2-site-x-a environment: <<: *secrets-store-env SCADABRIDGE_CONFIG: Site # Register row 63 — mirrors docker/docker-compose.yml. Enables the Development-only # DI scope validation (register row 50) on the site nodes, which previously ran # unvalidated because only the central containers set this. Dev/rig only. ASPNETCORE_ENVIRONMENT: Development ports: - "9121:8082" # Akka remoting - "9123:8083" # gRPC streaming volumes: - ./site-x-node-a/appsettings.Site.json:/app/appsettings.Site.json:ro - site-x-node-a-data:/app/data # Same named volume, mounted a second time OUTSIDE the content root (/app) for the # secrets store — see the x-secrets-store-env anchor above. - site-x-node-a-data:/data - ./site-x-node-a/logs:/app/logs networks: - scadabridge-net restart: unless-stopped site-x-b: image: scadabridge:latest init: true # PID-1 crash wedge, ScadaBridge#34 — see central-a # CoordinatedShutdown needs cluster-leave (15s budget) + cluster-exiting + # actor-system-terminate + Serilog flush; the 10s SIGTERM default SIGKILLed # mid-drain, turning every redeploy into the crash path (review 01 [Medium]). stop_grace_period: 30s container_name: scadabridge-env2-site-x-b environment: <<: *secrets-store-env SCADABRIDGE_CONFIG: Site # Register row 63 — mirrors docker/docker-compose.yml. Enables the Development-only # DI scope validation (register row 50) on the site nodes, which previously ran # unvalidated because only the central containers set this. Dev/rig only. ASPNETCORE_ENVIRONMENT: Development ports: - "9122:8082" # Akka remoting - "9124:8083" # gRPC streaming volumes: - ./site-x-node-b/appsettings.Site.json:/app/appsettings.Site.json:ro - site-x-node-b-data:/app/data # Same named volume, mounted a second time OUTSIDE the content root (/app) for the # secrets store — see the x-secrets-store-env anchor above. - site-x-node-b-data:/data - ./site-x-node-b/logs:/app/logs networks: - scadabridge-net restart: unless-stopped traefik: image: traefik:v3.4 container_name: scadabridge-env2-traefik ports: - "9100:80" # Env2 central load-balanced entrypoint - "8181:8080" # Env2 Traefik dashboard volumes: - ./traefik/traefik.yml:/etc/traefik/traefik.yml:ro - ./traefik/dynamic.yml:/etc/traefik/dynamic.yml:ro networks: - scadabridge-net restart: unless-stopped networks: scadabridge-net: external: true # Named volumes for the nodes' SQLite data dirs — mirrors docker/docker-compose.yml # (register row 79: bind mounts cross OrbStack's virtiofs, which does not give SQLite # true fsync durability). Host-side inspection via ~/OrbStack/docker/volumes/. # The central volumes are NEW with the Secrets 0.6.2 posture — env2 centrals previously # kept data on the container overlay (lost on recreate), which 0.6.2's store-path guard # exists to prevent. volumes: central-node-a-data: name: scadabridge-env2-central-node-a-data central-node-b-data: name: scadabridge-env2-central-node-b-data site-x-node-a-data: name: scadabridge-env2-site-x-node-a-data site-x-node-b-data: name: scadabridge-env2-site-x-node-b-data