Files
ScadaBridge/infra/docker-compose.yml
T
Joseph Doherty 8652eab98e fix(localdb): root-cause the soak disk-I/O failure + ship the hardening follow-ups
The Phase 2 soak's "LocalDb fails under load" blocker is NOT a product defect.
Root cause: host-side (macOS) sqlite3 reads of the live, bind-mounted WAL
databases. POSIX advisory locks do not propagate across the virtiofs boundary,
so the host reader believes it is the only connection, checkpoints on close and
resets the WAL to 0 bytes under the container. The container's still-mapped
WAL index then references frames that no longer exist and every subsequent
statement fails SQLITE_IOERR_SHORT_READ (522) -> primary code 10, permanently
until the process reopens the database.

Reproduced on demand both on the rig (one sqlite3 SELECT reset a 4.6 MiB WAL and
produced the first error one second later) and in a minimal python:3.12-alpine
repro with no LocalDb or .NET involved. Refuted the converse: a freshly-reopened
node sustained the full soak load 10+ minutes with zero errors.

The original brief's isolation was confounded - both nodes had been poisoned by
the same sampling pass, and a poisoned standby looks healthy only because it
issues almost no statements. Corrections annotated in place.

Hardening shipped alongside:
- SqliteErrorCodes.Describe: log SQLite primary AND extended codes at the
  LocalDb-adjacent catch sites (the missing extended code is what made the
  original diagnosis so slow).
- SiteAuditTelemetryActor: stop touching ActorContext across an await
  (NotSupportedException), with regression coverage.
- infra/reseed.sh: apply the MSSQL init scripts explicitly. The official
  mssql/server image does not implement /docker-entrypoint-initdb.d, so a fresh
  volume hung the reseed forever; compose mounts annotated as informational.

Deliberately NOT done: detect-and-reopen self-heal in SqliteLocalDb. It defends
only against external interference, which is now prevented at the source, and
same-kernel production readers see the locks correctly.

Build 0 warnings; SiteAuditTelemetryActorTests 9/9, SiteEventLogging 70/70.

Claude-Session: https://claude.ai/code/session_01BL2Vu1ESDQ9SCN4gVKkdts
2026-07-20 02:01:00 -04:00

128 lines
3.3 KiB
YAML

services:
opcua:
image: mcr.microsoft.com/iotedge/opc-plc:latest
container_name: scadabridge-opcua
ports:
- "50000:50000"
- "8080:8080"
volumes:
- ./opcua/nodes.json:/app/config/nodes.json:ro
command: >
--autoaccept
--unsecuretransport
--sph
--sn=5 --sr=10 --st=uint
--fn=5 --fr=1 --ft=uint
--gn=5
--nf=/app/config/nodes.json
--pn=50000
networks:
- scadabridge-net
restart: unless-stopped
opcua2:
image: mcr.microsoft.com/iotedge/opc-plc:latest
container_name: scadabridge-opcua2
ports:
- "50010:50010"
- "8081:8080"
volumes:
- ./opcua/nodes.json:/app/config/nodes.json:ro
command: >
--autoaccept
--unsecuretransport
--sph
--sn=5 --sr=10 --st=uint
--fn=5 --fr=1 --ft=uint
--gn=5
--nf=/app/config/nodes.json
--pn=50010
networks:
- scadabridge-net
restart: unless-stopped
# RETIRED 2026-06-04: superseded by the shared dev GLAuth on 10.100.0.35:3893
# (scadaproj/infra/glauth/). The central nodes now bind there (see
# docker/ + docker-env2 central-node appsettings: Ldap:Server=10.100.0.35).
# Kept here, commented, for rollback — uncomment + `docker compose up -d ldap`
# and revert the central-node Server back to "scadabridge-ldap".
# ldap:
# image: glauth/glauth:latest
# container_name: scadabridge-ldap
# ports:
# - "3893:3893"
# volumes:
# - ./glauth/config.toml:/app/config/config.cfg:ro
# networks:
# - scadabridge-net
# restart: unless-stopped
mssql:
image: mcr.microsoft.com/mssql/server:2022-latest
container_name: scadabridge-mssql
ports:
- "1433:1433"
environment:
ACCEPT_EULA: "Y"
MSSQL_SA_PASSWORD: "ScadaBridge_Dev1#"
MSSQL_PID: "Developer"
volumes:
- scadabridge-mssql-data:/var/opt/mssql
# NOTE: the official mssql/server image does NOT run
# /docker-entrypoint-initdb.d — these mounts are informational only;
# infra/reseed.sh applies the scripts explicitly via sqlcmd.
- ./mssql/setup.sql:/docker-entrypoint-initdb.d/setup.sql:ro
- ./mssql/machinedata_seed.sql:/docker-entrypoint-initdb.d/machinedata_seed.sql:ro
- ./mssql/setup-env2.sql:/docker-entrypoint-initdb.d/setup-env2.sql:ro
networks:
- scadabridge-net
restart: unless-stopped
smtp:
image: axllent/mailpit:latest
container_name: scadabridge-smtp
ports:
- "1025:1025"
- "8025:8025"
environment:
MP_SMTP_AUTH_ACCEPT_ANY: 1
MP_SMTP_AUTH_ALLOW_INSECURE: 1
MP_MAX_MESSAGES: 500
networks:
- scadabridge-net
restart: unless-stopped
restapi:
build: ./restapi
container_name: scadabridge-restapi
ports:
- "5200:5200"
environment:
API_NO_AUTH: 0
PORT: 5200
networks:
- scadabridge-net
restart: unless-stopped
playwright:
image: mcr.microsoft.com/playwright:v1.58.2-noble
container_name: scadabridge-playwright
ports:
- "3000:3000"
command: >
npx -y playwright@1.58.2 run-server
--host 0.0.0.0
--port 3000
ipc: host
networks:
- scadabridge-net
restart: unless-stopped
volumes:
scadabridge-mssql-data:
networks:
scadabridge-net:
name: scadabridge-net
external: true