473 lines
28 KiB
YAML
473 lines
28 KiB
YAML
# 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-<siteId> 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-<siteId> 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/<name>;
|
|
# `docker compose down` preserves them; remove with `docker volume rm <name>`.
|
|
# 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
|