# EVERY node needs a secret store path; only four take the hub anchor below. Keep this # anchor separate from that one — folding it in is what crash-looped site-b and site-c on # 2026-08-11: they merge no secrets anchor, fell back to the shipped default, and were # rejected at boot. # # The value must be absolute AND outside the content root (/app) — Secrets 0.6.2 validates # both at startup, because a store inside the deployment directory is destroyed by an # ordinary upgrade (this wiped the MxGateway key store on 2026-08-09). /data is the node's # own data volume (named volume since register row 79), the SAME volume as /app/data, so an # existing store is found unmoved; only the path used to reach it changes. Do NOT point # this back at /app/data — absolute is not sufficient, and the boot will fail. # # Do NOT delete this key expecting the library's per-user default to take over. Measured on # this rig: with a Secrets section present but no SqlitePath, the effective value is the # RELATIVE "secrets.db", which fails both rules. Setting it explicitly is the supported # posture for a deployment in any case. x-secrets-store-env: &secrets-store-env Secrets__SqlitePath: "/data/scadabridge-secrets.db" # ── Secrets:Store:Provider=SqlServer: central shares one secret store directly (Task 5) ── # # Task 11 retired secrets replication entirely — both the pull-only gRPC hub (central # hosted, sites swept it) and the SQL-Server ReplicatingSecretStore mode that preceded it. # Sites now receive secrets exclusively via Deploy Artifacts; there is no hub and nothing # for a site to sweep. What remains, and is unrelated to replication, is Secrets:Store: # Provider=SqlServer: it points BOTH central nodes at ONE shared SQL-Server database # directly — no hub, no follower sweep, just two nodes reading and writing the same rows — # so a clustered central pair can never serve sites a drifted secret set. central-a/b set # it explicitly below; site nodes always ignore the key and keep their local SQLite store. # # ZbSecretsHub is a dedicated database on the rig's existing scadabridge-mssql container # (default zbsecrets schema, created by the boot-time SqlServerSecretsStoreMigrator; the # database itself + the scadabridge_app grant were provisioned once via sqlcmd — see # docs/plans/2026-08-07-secrets-central-shared-store-live-gate.md). Same DEV-ONLY # credentials as the ScadaBridge__Database__* strings below. Must be a LITERAL value, # never a ${secret:} reference — the pre-host expander needs this string to reach the # store that would resolve it (bootstrap circularity; registration rejects it). Site nodes # must NEVER carry this key: sites talk to central, not to central's database. x-secrets-central-store-env: &secrets-central-store-env Secrets__Store__Provider: "SqlServer" Secrets__SqlServer__ConnectionString: "Server=scadabridge-mssql,1433;Database=ZbSecretsHub;User Id=scadabridge_app;Password=ScadaBridge_Dev1#;TrustServerCertificate=true" # ── Deploy-artifact secret KEKs ────────────────────────────────────────────────────── # # Every node that HANDLES a deployed secret — central (it encrypts what ships in the # artifact) and site-a (it decrypts what it receives) — must resolve the SAME # ZB_SECRETS_MASTER_KEY: only ciphertext ever crosses the wire, so a node with a different # KEK fails closed on resolve with a kek_id mismatch that reads like corruption but is a # deployment error. site-b/site-c deliberately carry a DIFFERENT dev KEK (below) so that # mismatch is a real, provable negative on this rig rather than an assumption — see # Task 13. # # ALL VALUES HERE ARE DEV-ONLY and committed under the same exception as the mesh PSKs # and the ApiKeyPepper below: a local docker rig needs a working credential in source # control to boot. Production supplies the KEK out of band (ZB_SECRETS_MASTER_KEY, never # committed). x-secrets-shared-kek-env: &secrets-shared-kek-env # DEV-ONLY KEK — NOT a real key. Identical on central-a/b and site-a-a/b. ZB_SECRETS_MASTER_KEY: "zZiBWuoaVMbJmGXToLk9Lakw0iJozXoL/7Gxac3GwJ4=" # A DIFFERENT DEV-ONLY KEK — NOT a real key — deliberately mismatched from the one above. # site-b and site-c never receive a deployed secret from this rig's exercised paths today, # but carrying a mismatched key here (rather than none) means a future deploy targeting # them fails closed with a named kek_id mismatch instead of an untested code path. x-secrets-mismatched-kek-env: &secrets-mismatched-kek-env ZB_SECRETS_MASTER_KEY: "esKrmqV2RBmMq/+6T/5zK2BcalRy5gzDXhoG9Qydg6Q=" services: central-a: image: scadabridge:latest # An init process (tini) as PID 1, so a crashed dotnet process actually dies. # Without it dotnet IS PID 1, Linux ignores the SIGABRT the runtime's crash path # raises against PID 1, and any unhandled boot exception left the container # `running` with the main thread spinning at 100% CPU — restart policy never # fired (ScadaBridge#34). Belt to Program.cs's UnhandledException handler, which # covers managed exceptions but not FailFast/runtime-internal aborts. 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-central-a environment: # Central-only shared SQL secret store (Secrets:Store:Provider=SqlServer, Task 5) + the # deploy-artifact secret KEK (anchors at the top of this file). <<: [*secrets-store-env, *secrets-shared-kek-env, *secrets-central-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). Real deployments inject a true secret # out-of-band (env/secret store), never from source control — see # docs/operations/inbound-api-key-reissue.md. Both Central nodes share one pepper. ScadaBridge__InboundApi__ApiKeyPepper: "dev-only-insecure-pepper-docker-cluster-0001" # Shared inbound API key store (design 2026-09-02, register row 108): both centrals read and # write ONE zbauth schema in the configuration database, so a key minted on either node # survives a central failover and a revoke is cluster-wide. The SqlServer connection string # is deliberately NOT set: it defaults to ScadaBridge__Database__ConfigurationDb below. # Each node's legacy per-node SQLite file is imported once at boot and renamed *.imported. ScadaBridge__InboundApi__ApiKeyStore__Provider: "SqlServer" # DEV-ONLY local-cluster secrets — NOT real secrets. Relocated out of the mounted # appsettings.Central.json (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, so the dev cluster boots with no KEK/secret store. The SQL password is # the same value already committed for the sibling scadabridge-mssql container # (infra/docker-compose.yml MSSQL_SA_PASSWORD) — consolidation, not new exposure. Real # deployments use ${secret:} tokens (src/.../appsettings.Central.json) + a seeded store. ScadaBridge__Database__ConfigurationDb: "Server=scadabridge-mssql,1433;Database=ScadaBridgeConfig;User Id=scadabridge_app;Password=ScadaBridge_Dev1#;TrustServerCertificate=true" ScadaBridge__Database__MachineDataDb: "Server=scadabridge-mssql,1433;Database=ScadaBridgeMachineData;User Id=scadabridge_app;Password=ScadaBridge_Dev1#;TrustServerCertificate=true" ScadaBridge__Security__Ldap__ServiceAccountPassword: "serviceaccount123" ScadaBridge__Security__JwtSigningKey: "scadabridge-dev-jwt-signing-key-must-be-at-least-32-characters-long" # DEV-ONLY gRPC control-plane preshared keys, one per site — NOT real secrets. # Central verifies/presents these; each site node carries the same value as # ScadaBridge:Communication:GrpcPsk in its mounted appsettings.Site.json. Kept as # env overrides (not in the mounted central appsettings) so that file stays free of # plaintext credentials. Production instead seeds SB-GRPC-PSK- into the # secret store, which is also the only source that can serve a site added at runtime. ScadaBridge__Communication__SitePsks__site-a: "dev-grpc-psk-docker-site-a" ScadaBridge__Communication__SitePsks__site-b: "dev-grpc-psk-docker-site-b" ScadaBridge__Communication__SitePsks__site-c: "dev-grpc-psk-docker-site-c" ports: - "9001:5000" # Web UI + Inbound API - "9011:8081" # Akka remoting (host access for CLI/debugging) - "9013:8083" # gRPC control plane (CentralControlService, T1A.2) volumes: - ./central-node-a/appsettings.Central.json:/app/appsettings.Central.json:ro # Originally added for the gRPC secrets hub's local SQLite store; since the # central store moved to the shared SQL-Server database (Secrets 0.5.0, # scadaproj#4) the scadabridge-secrets.db here is a pre-0.5.0 residue, but the # volume is still needed (inbound-api-keys.sqlite lives on it). - central-node-a-data:/app/data # Same named volume, mounted a second time OUTSIDE the content root (/app). # Secrets 0.6.2 refuses a store path under the content root; this mount is # not touched by an image rebuild, but the guard is lexical and cannot know that. - 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-central-b environment: # Central-only shared SQL secret store (Secrets:Store:Provider=SqlServer, Task 5) + the # deploy-artifact secret KEK (anchors at the top of this file). <<: [*secrets-store-env, *secrets-shared-kek-env, *secrets-central-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). Real deployments inject a true secret # out-of-band (env/secret store), never from source control — see # docs/operations/inbound-api-key-reissue.md. Both Central nodes share one pepper. ScadaBridge__InboundApi__ApiKeyPepper: "dev-only-insecure-pepper-docker-cluster-0001" # Shared inbound API key store (design 2026-09-02, register row 108): both centrals read and # write ONE zbauth schema in the configuration database, so a key minted on either node # survives a central failover and a revoke is cluster-wide. The SqlServer connection string # is deliberately NOT set: it defaults to ScadaBridge__Database__ConfigurationDb below. # Each node's legacy per-node SQLite file is imported once at boot and renamed *.imported. ScadaBridge__InboundApi__ApiKeyStore__Provider: "SqlServer" # DEV-ONLY local-cluster secrets — NOT real secrets. Relocated out of the mounted # appsettings.Central.json (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, so the dev cluster boots with no KEK/secret store. The SQL password is # the same value already committed for the sibling scadabridge-mssql container # (infra/docker-compose.yml MSSQL_SA_PASSWORD) — consolidation, not new exposure. Real # deployments use ${secret:} tokens (src/.../appsettings.Central.json) + a seeded store. ScadaBridge__Database__ConfigurationDb: "Server=scadabridge-mssql,1433;Database=ScadaBridgeConfig;User Id=scadabridge_app;Password=ScadaBridge_Dev1#;TrustServerCertificate=true" ScadaBridge__Database__MachineDataDb: "Server=scadabridge-mssql,1433;Database=ScadaBridgeMachineData;User Id=scadabridge_app;Password=ScadaBridge_Dev1#;TrustServerCertificate=true" ScadaBridge__Security__Ldap__ServiceAccountPassword: "serviceaccount123" ScadaBridge__Security__JwtSigningKey: "scadabridge-dev-jwt-signing-key-must-be-at-least-32-characters-long" # DEV-ONLY gRPC control-plane preshared keys, one per site — NOT real secrets. # Central verifies/presents these; each site node carries the same value as # ScadaBridge:Communication:GrpcPsk in its mounted appsettings.Site.json. Kept as # env overrides (not in the mounted central appsettings) so that file stays free of # plaintext credentials. Production instead seeds SB-GRPC-PSK- into the # secret store, which is also the only source that can serve a site added at runtime. ScadaBridge__Communication__SitePsks__site-a: "dev-grpc-psk-docker-site-a" ScadaBridge__Communication__SitePsks__site-b: "dev-grpc-psk-docker-site-b" ScadaBridge__Communication__SitePsks__site-c: "dev-grpc-psk-docker-site-c" ports: - "9002:5000" # Web UI + Inbound API - "9012:8081" # Akka remoting - "9014:8083" # gRPC control plane (CentralControlService, T1A.2) volumes: - ./central-node-b/appsettings.Central.json:/app/appsettings.Central.json:ro # Originally added for the gRPC secrets hub's local SQLite store; since the # central store moved to the shared SQL-Server database (Secrets 0.5.0, # scadaproj#4) the scadabridge-secrets.db here is a pre-0.5.0 residue, but the # volume is still needed (inbound-api-keys.sqlite lives on it). - central-node-b-data:/app/data # Same named volume, mounted a second time OUTSIDE the content root (/app). # Secrets 0.6.2 refuses a store path under the content root; this mount is # not touched by an image rebuild, but the guard is lexical and cannot know that. - central-node-b-data:/data - ./central-node-b/logs:/app/logs networks: - scadabridge-net restart: unless-stopped site-a-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-site-a-a environment: # The shared deploy-artifact secret KEK (anchors at the top of this file) — this node # must decrypt what central ships in a deploy artifact. site-b and site-c deliberately # carry a DIFFERENT (mismatched) KEK instead, so that failure mode is provable on this # rig rather than assumed (Task 13). <<: [*secrets-store-env, *secrets-shared-kek-env] SCADABRIDGE_CONFIG: Site # Register row 63. The central containers have always set this; the site containers # never did, so the Development-only DI scope validation (register row 50, # ServiceProviderValidation.Configure — ValidateScopes/ValidateOnBuild) was dead on # exactly the half of the topology that has six nodes. The site graph now validates # clean, so turning it on here buys live coverage: a captive dependency or an # unconstructible site-only descriptor fails the container at boot instead of # surfacing as a mid-run resolve error. Dev/rig only — never set in production. ASPNETCORE_ENVIRONMENT: Development ports: - "9021:8082" # Akka remoting (host access for debugging) - "9023:8083" # gRPC streaming volumes: - ./site-a-node-a/appsettings.Site.json:/app/appsettings.Site.json:ro - ./recipes:/app/recipes:ro # script file access (Files global) — sample Delmia recipes - site-a-node-a-data:/app/data # Same named volume, mounted a second time OUTSIDE the content root (/app). # Secrets 0.6.2 refuses a store path under the content root; this mount is # not touched by an image rebuild, but the guard is lexical and cannot know that. - site-a-node-a-data:/data - ./site-a-node-a/logs:/app/logs networks: - scadabridge-net restart: unless-stopped site-a-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-site-a-b environment: # The shared deploy-artifact secret KEK (anchors at the top of this file) — this node # must decrypt what central ships in a deploy artifact. site-b and site-c deliberately # carry a DIFFERENT (mismatched) KEK instead, so that failure mode is provable on this # rig rather than assumed (Task 13). <<: [*secrets-store-env, *secrets-shared-kek-env] SCADABRIDGE_CONFIG: Site # Register row 63. The central containers have always set this; the site containers # never did, so the Development-only DI scope validation (register row 50, # ServiceProviderValidation.Configure — ValidateScopes/ValidateOnBuild) was dead on # exactly the half of the topology that has six nodes. The site graph now validates # clean, so turning it on here buys live coverage: a captive dependency or an # unconstructible site-only descriptor fails the container at boot instead of # surfacing as a mid-run resolve error. Dev/rig only — never set in production. ASPNETCORE_ENVIRONMENT: Development ports: - "9022:8082" # Akka remoting - "9024:8083" # gRPC streaming volumes: - ./site-a-node-b/appsettings.Site.json:/app/appsettings.Site.json:ro - ./recipes:/app/recipes:ro # script file access (Files global) — sample Delmia recipes - site-a-node-b-data:/app/data # Same named volume, mounted a second time OUTSIDE the content root (/app). # Secrets 0.6.2 refuses a store path under the content root; this mount is # not touched by an image rebuild, but the guard is lexical and cannot know that. - site-a-node-b-data:/data - ./site-a-node-b/logs:/app/logs networks: - scadabridge-net restart: unless-stopped site-b-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-site-b-a environment: # A DIFFERENT (mismatched) deploy-artifact secret KEK from central/site-a's — see the # anchor comment at the top of this file (Task 13's mismatch negative). <<: [*secrets-store-env, *secrets-mismatched-kek-env] SCADABRIDGE_CONFIG: Site # Register row 63. The central containers have always set this; the site containers # never did, so the Development-only DI scope validation (register row 50, # ServiceProviderValidation.Configure — ValidateScopes/ValidateOnBuild) was dead on # exactly the half of the topology that has six nodes. The site graph now validates # clean, so turning it on here buys live coverage: a captive dependency or an # unconstructible site-only descriptor fails the container at boot instead of # surfacing as a mid-run resolve error. Dev/rig only — never set in production. ASPNETCORE_ENVIRONMENT: Development ports: - "9031:8082" # Akka remoting - "9033:8083" # gRPC streaming volumes: - ./site-b-node-a/appsettings.Site.json:/app/appsettings.Site.json:ro - site-b-node-a-data:/app/data # Same named volume, mounted a second time OUTSIDE the content root (/app). # Secrets 0.6.2 refuses a store path under the content root; this mount is # not touched by an image rebuild, but the guard is lexical and cannot know that. - site-b-node-a-data:/data - ./site-b-node-a/logs:/app/logs networks: - scadabridge-net restart: unless-stopped site-b-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-site-b-b environment: # A DIFFERENT (mismatched) deploy-artifact secret KEK from central/site-a's — see the # anchor comment at the top of this file (Task 13's mismatch negative). <<: [*secrets-store-env, *secrets-mismatched-kek-env] SCADABRIDGE_CONFIG: Site # Register row 63. The central containers have always set this; the site containers # never did, so the Development-only DI scope validation (register row 50, # ServiceProviderValidation.Configure — ValidateScopes/ValidateOnBuild) was dead on # exactly the half of the topology that has six nodes. The site graph now validates # clean, so turning it on here buys live coverage: a captive dependency or an # unconstructible site-only descriptor fails the container at boot instead of # surfacing as a mid-run resolve error. Dev/rig only — never set in production. ASPNETCORE_ENVIRONMENT: Development ports: - "9032:8082" # Akka remoting - "9034:8083" # gRPC streaming volumes: - ./site-b-node-b/appsettings.Site.json:/app/appsettings.Site.json:ro - site-b-node-b-data:/app/data # Same named volume, mounted a second time OUTSIDE the content root (/app). # Secrets 0.6.2 refuses a store path under the content root; this mount is # not touched by an image rebuild, but the guard is lexical and cannot know that. - site-b-node-b-data:/data - ./site-b-node-b/logs:/app/logs networks: - scadabridge-net restart: unless-stopped site-c-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-site-c-a environment: # A DIFFERENT (mismatched) deploy-artifact secret KEK from central/site-a's — see the # anchor comment at the top of this file (Task 13's mismatch negative). <<: [*secrets-store-env, *secrets-mismatched-kek-env] SCADABRIDGE_CONFIG: Site # Register row 63. The central containers have always set this; the site containers # never did, so the Development-only DI scope validation (register row 50, # ServiceProviderValidation.Configure — ValidateScopes/ValidateOnBuild) was dead on # exactly the half of the topology that has six nodes. The site graph now validates # clean, so turning it on here buys live coverage: a captive dependency or an # unconstructible site-only descriptor fails the container at boot instead of # surfacing as a mid-run resolve error. Dev/rig only — never set in production. ASPNETCORE_ENVIRONMENT: Development ports: - "9041:8082" # Akka remoting - "9043:8083" # gRPC streaming volumes: - ./site-c-node-a/appsettings.Site.json:/app/appsettings.Site.json:ro - site-c-node-a-data:/app/data # Same named volume, mounted a second time OUTSIDE the content root (/app). # Secrets 0.6.2 refuses a store path under the content root; this mount is # not touched by an image rebuild, but the guard is lexical and cannot know that. - site-c-node-a-data:/data - ./site-c-node-a/logs:/app/logs networks: - scadabridge-net restart: unless-stopped site-c-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-site-c-b environment: # A DIFFERENT (mismatched) deploy-artifact secret KEK from central/site-a's — see the # anchor comment at the top of this file (Task 13's mismatch negative). <<: [*secrets-store-env, *secrets-mismatched-kek-env] SCADABRIDGE_CONFIG: Site # Register row 63. The central containers have always set this; the site containers # never did, so the Development-only DI scope validation (register row 50, # ServiceProviderValidation.Configure — ValidateScopes/ValidateOnBuild) was dead on # exactly the half of the topology that has six nodes. The site graph now validates # clean, so turning it on here buys live coverage: a captive dependency or an # unconstructible site-only descriptor fails the container at boot instead of # surfacing as a mid-run resolve error. Dev/rig only — never set in production. ASPNETCORE_ENVIRONMENT: Development ports: - "9042:8082" # Akka remoting - "9044:8083" # gRPC streaming volumes: - ./site-c-node-b/appsettings.Site.json:/app/appsettings.Site.json:ro - site-c-node-b-data:/app/data # Same named volume, mounted a second time OUTSIDE the content root (/app). # Secrets 0.6.2 refuses a store path under the content root; this mount is # not touched by an image rebuild, but the guard is lexical and cannot know that. - site-c-node-b-data:/data - ./site-c-node-b/logs:/app/logs networks: - scadabridge-net restart: unless-stopped traefik: image: traefik:v3.4 container_name: scadabridge-traefik ports: - "9000:80" # Central load-balanced entrypoint - "8180:8080" # 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 every node's SQLite data dir (register row 79). On macOS/OrbStack a # bind mount crosses virtiofs, which does not give SQLite true fsync durability — both # site-a LocalDb files corrupted through that layer (row 69). Named volumes live on # native ext4 inside the OrbStack VM, taking virtiofs out of the write path entirely. # Host-side inspection still works read-only via ~/OrbStack/docker/volumes/; # `docker compose down` preserves them; remove with `docker volume rm `. # The appsettings/logs bind mounts stay: config is ro, and Serilog's append-only files # have no fsync-durability requirement worth losing `tail -f` from the repo tree for. volumes: central-node-a-data: name: scadabridge-central-node-a-data central-node-b-data: name: scadabridge-central-node-b-data site-a-node-a-data: name: scadabridge-site-a-node-a-data site-a-node-b-data: name: scadabridge-site-a-node-b-data site-b-node-a-data: name: scadabridge-site-b-node-a-data site-b-node-b-data: name: scadabridge-site-b-node-b-data site-c-node-a-data: name: scadabridge-site-c-node-a-data site-c-node-b-data: name: scadabridge-site-c-node-b-data