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 sqlcmdon Mac, or install go-sqlcmd manually - A
.bakfile 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 pscube2-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.shSanity 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.sqlExpected: 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.shThis sets:
ConnectionStrings:eManEx— points atlocalhost,1433/BcomMX_01_04Jwt:SigningKey— dev-only 53-char stringMachineKey:ValidationKey— frommanexcloud/newSite/Web.config(ADR 0001)MachineKey:DecryptionKey— same sourceFeatureFlags:EnableGroupWrites—truesoPOST /api/v1/groupsisn’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 runExpected:
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 UIThen 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>"}' | jqaccessTokenin 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_Membershipshape (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-mssqlStart it back up:
docker start cube2-mssqlNuke everything (frees Mac disk):
docker rm -f cube2-mssql
rm -rf ~/mssql-data ~/mssql-backupsTroubleshooting#
| Symptom | Cause | Fix |
|---|---|---|
Container exits within seconds, logs show Invalid mapping of address | Running mssql/server image on Apple Silicon without Rosetta | Use azure-sql-edge (arm64-native) as above, or enable Rosetta in Docker Desktop → Settings → General |
RESTORE: operating system error 31 while attempting to expand | Docker Desktop VM disk full | Mount ~/mssql-data onto Mac disk (Step 1) or bump Docker Desktop → Settings → Resources → Disk image size |
sqlcmd: TLS Handshake failed: x509: negative serial number | Modern go-sqlcmd rejecting SQL Edge’s dev cert | Use GODEBUG=x509negativeserial=1 (already in ~/mssql.sh) |
sqlcmd: paste breaks command mid-line | Terminal auto-wraps long lines with real newlines | Use 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 2019 | Ask ops for a 2019-compatible backup |
dotnet run: Jwt:SigningKey must be set (min 32 chars) | ASPNETCORE_ENVIRONMENT not Development → user-secrets not loaded | Ensure src/Manex.Web/Properties/launchSettings.json exists (it should — it’s committed) |
| Login returns 401 with a real user + correct password | PasswordHasher.VerifyEncrypted doesn’t match the ASP.NET Membership algorithm used on the source system | Staging-validation moment — see ADR 0001 amendment; adjust VerifyEncrypted if the byte layout differs |
Group writes disabled on POST/PUT/DELETE /api/v1/groups | FeatureFlags:EnableGroupWrites not set | Re-run ./scripts/set-dev-secrets.sh |
| Group write returns 403 | JWT missing mod_admin claim | Log 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.shscripts/set-dev-secrets.sh— sets all user-secrets in one shotsrc/Manex.Web/Properties/launchSettings.json— makesdotnet rundefault to Developmentdeploy/sql/phase-1-add-mnxuserauth.sql— idempotent MnxUserAuth DDLdeploy/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