Local dev against a restored .bak#

Restore a copy of BcomMX_01_04 (or any staging .bak) into a local SQL container and point the API at it. Real users, real roles, real data — same shape as staging, no VPN required.

This is Mode C — a middle ground between Mode A (empty sandbox) and Mode B (against real staging).

Prerequisites#

  • Docker Desktop running
  • .NET 8 SDK — dotnet --version → 8.x
  • sqlcmd on your host — brew install sqlcmd on Mac, or install go-sqlcmd manually
  • A .bak file from staging (ask ops if you don’t have one)
  • ~30 GB free disk space

Apple Silicon caveat: mcr.microsoft.com/mssql/server:2022-latest and 2019-latest don’t run reliably under QEMU — they crash at startup with “Invalid mapping of address”. We use azure-sql-edge (ARM64-native) instead. SQL Edge is a subset of SQL Server but restores .bak files fine as long as the source DB is compatible with SQL 2019.

Step 1 — Start the container#

Backup + data both mounted on the Mac disk so they survive container restarts and don’t consume Docker Desktop’s tiny VM disk:

mkdir -p ~/mssql-backups ~/mssql-data

docker run -d --name cube2-mssql \
  -e 'ACCEPT_EULA=1' \
  -e 'MSSQL_SA_PASSWORD=Cube2!DevPwd' \
  -p 1433:1433 \
  -v ~/mssql-backups:/var/opt/mssql/backup \
  -v ~/mssql-data:/var/opt/mssql/data \
  mcr.microsoft.com/azure-sql-edge:latest

sleep 25
docker ps

cube2-mssql should show STATUS = Up.

Step 2 — Get the .bak into ~/mssql-backups#

mv /path/to/your/cube_backup.bak ~/mssql-backups/

The mount is live — no container restart needed. Verify:

docker exec cube2-mssql ls -lh /var/opt/mssql/backup/

Step 3 — Set up the sqlcmd wrapper#

~/mssql.sh — a one-line wrapper so every command isn’t 200 characters:

cp scripts/sqlcmd-local.sh ~/mssql.sh
chmod +x ~/mssql.sh

Sanity check:

~/mssql.sh -Q "SELECT @@VERSION"

Should print Microsoft Azure SQL Edge ….

The wrapper’s GODEBUG=x509negativeserial=1 env is required because SQL Edge ships with a self-signed cert that has a negative serial number, which the modern Go crypto library (used by go-sqlcmd) refuses to parse. Dev-only workaround; production TLS is unaffected.

Step 4 — Get the backup’s logical file names#

~/mssql.sh -Q "RESTORE FILELISTONLY FROM DISK = '/var/opt/mssql/backup/cube_backup.bak'"

Two rows in the output. Note the LogicalName values (usually MANEX and MANEX_log for cube backups; may differ).

Step 5 — Restore into BcomMX_01_04#

Replace MANEX / MANEX_log with whatever you saw in Step 4:

~/mssql.sh -Q "RESTORE DATABASE [BcomMX_01_04] FROM DISK = '/var/opt/mssql/backup/cube_backup.bak' WITH MOVE 'MANEX' TO '/var/opt/mssql/data/BcomMX_01_04.mdf', MOVE 'MANEX_log' TO '/var/opt/mssql/data/BcomMX_01_04_log.ldf', REPLACE, STATS = 10;"

Takes 3–8 minutes for ~30 GB. Watch for 10 percent processed. progress lines, ending with RESTORE DATABASE successfully processed ....

Verify:

~/mssql.sh -Q "SELECT name, state_desc FROM sys.databases WHERE name='BcomMX_01_04'"

Want state_desc = ONLINE.

Sanity-check the row counts:

~/mssql.sh -d BcomMX_01_04 -Q "SELECT (SELECT COUNT(*) FROM aspnet_Users) users, (SELECT COUNT(*) FROM aspnet_Roles) roles, (SELECT COUNT(*) FROM MnxModule) modules"

All three should be > 0.

Step 6 — Apply Phase 1 SQL deltas#

Cube 2.0 owns MnxUserAuth (rehashed BCrypt passwords, per ADR 0006). Idempotent, safe to re-run:

~/mssql.sh -d BcomMX_01_04 -i deploy/sql/phase-1-add-mnxuserauth.sql

Expected: MnxUserAuth created. (or already exists — no-op.).

Step 7 — Configure the app#

The repo ships a script that sets every dev secret in one shot:

./scripts/set-dev-secrets.sh

This sets:

  • ConnectionStrings:eManEx — points at localhost,1433 / BcomMX_01_04
  • Jwt:SigningKey — dev-only 53-char string
  • MachineKey:ValidationKey — from manexcloud/newSite/Web.config (ADR 0001)
  • MachineKey:DecryptionKey — same source
  • FeatureFlags:EnableGroupWrites — true so POST /api/v1/groups isn’t gated

The values live in ~/.microsoft/usersecrets/<user-secrets-id>/secrets.json (or %APPDATA% on Windows) — outside the repo, never committed.

Step 8 — Run#

cd src/Manex.Web
dotnet run

Expected:

Now listening on: http://localhost:5000
Application started. Press Ctrl+C to shut down.

Step 9 — Verify from a second terminal#

curl -sS http://localhost:5000/health    # Healthy
curl -sS http://localhost:5000/ready     # Healthy — proves the SQL connection
open http://localhost:5000/docs          # Swagger UI

Then try a real login. Find a username in the restored DB first:

~/mssql.sh -d BcomMX_01_04 -Q "SELECT TOP 3 UserName FROM aspnet_Users ORDER BY LastActivityDate DESC"

If you know a plaintext password for one of them:

curl -sS -X POST http://localhost:5000/api/v1/auth/login \
  -H "Content-Type: application/json" \
  -d '{"username":"<real-user>","password":"<their-password>"}' | jq
  • accessToken in the response → auth stack works end-to-end including the legacy encrypted-password verify.
  • HTTP 401 “Invalid credentials” → either wrong password, or the encrypted-verify algorithm needs adjustment against real staging output. Send one row of aspnet_Membership shape (password length, salt length, format — never the actual value) to whoever’s driving Phase 1 auth work.

Everyday operations#

Stop the container (data survives on Mac disk):

docker stop cube2-mssql

Start it back up:

docker start cube2-mssql

Nuke everything (frees Mac disk):

docker rm -f cube2-mssql
rm -rf ~/mssql-data ~/mssql-backups

Troubleshooting#

SymptomCauseFix
Container exits within seconds, logs show Invalid mapping of addressRunning mssql/server image on Apple Silicon without RosettaUse azure-sql-edge (arm64-native) as above, or enable Rosetta in Docker Desktop → Settings → General
RESTORE: operating system error 31 while attempting to expandDocker Desktop VM disk fullMount ~/mssql-data onto Mac disk (Step 1) or bump Docker Desktop → Settings → Resources → Disk image size
sqlcmd: TLS Handshake failed: x509: negative serial numberModern go-sqlcmd rejecting SQL Edge’s dev certUse GODEBUG=x509negativeserial=1 (already in ~/mssql.sh)
sqlcmd: paste breaks command mid-lineTerminal auto-wraps long lines with real newlinesUse the wrapper ~/mssql.sh or write commands to a .sql file and use -i
RESTORE: The database was backed up on a server running version 16.x.bak from SQL 2022, SQL Edge is 2019Ask ops for a 2019-compatible backup
dotnet run: Jwt:SigningKey must be set (min 32 chars)ASPNETCORE_ENVIRONMENT not Development → user-secrets not loadedEnsure src/Manex.Web/Properties/launchSettings.json exists (it should — it’s committed)
Login returns 401 with a real user + correct passwordPasswordHasher.VerifyEncrypted doesn’t match the ASP.NET Membership algorithm used on the source systemStaging-validation moment — see ADR 0001 amendment; adjust VerifyEncrypted if the byte layout differs
Group writes disabled on POST/PUT/DELETE /api/v1/groupsFeatureFlags:EnableGroupWrites not setRe-run ./scripts/set-dev-secrets.sh
Group write returns 403JWT missing mod_admin claimLog in as a user with any admin flag on aspnet_Profile (CompanyAdmin, AcctAdmin, etc.)

What’s committed vs what’s local#

Committed to the repo:

  • scripts/sqlcmd-local.sh — the wrapper template you copy to ~/mssql.sh
  • scripts/set-dev-secrets.sh — sets all user-secrets in one shot
  • src/Manex.Web/Properties/launchSettings.json — makes dotnet run default to Development
  • deploy/sql/phase-1-add-mnxuserauth.sql — idempotent MnxUserAuth DDL
  • deploy/sql/phase-1-schema-audit.sql — read-only ADR 0005 audit queries

Local only (never committed):

  • ~/mssql-backups/*.bak — the actual backup file
  • ~/mssql-data/ — restored .mdf / .ldf
  • ~/mssql.sh — your local wrapper with the dev password
  • ~/.microsoft/usersecrets/…/secrets.json — the user-secrets store