Operations runbook¶
First-time bootstrap¶
- Generate the auth secrets:
python -c "import secrets; print(secrets.token_urlsafe(32))" # JWT secret
python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())" # Fernet key
- Copy
.env.example→.envand fill in:
DASHBOARD_VIEWER_PASSWORD=...
DASHBOARD_ADMIN_PASSWORD=...
DASHBOARD_JWT_SECRET=...
DASHBOARD_SECRET_KEY=...
-
Bring up the stack:
make stack-up. The dashboard creates its tables and seeds the three secret-key rows on first boot. -
Sign in at http://localhost:18099 — first with the viewer password to verify the read path, then with the admin password.
-
As admin, set the Grafana API key (Config → Credentials). Confirm by loading a Grafana panel via
/api/proxy/grafana/...(or the Models page if it routes through Grafana).
Running it without Docker¶
For frontend or backend work, run the two halves directly. The backend needs the same four secrets as the container; an in-memory database keeps the run throwaway.
cd platform/services/dashboard/backend
export DATABASE_URL=sqlite+aiosqlite:///:memory:
export DASHBOARD_VIEWER_PASSWORD=v DASHBOARD_ADMIN_PASSWORD=a
export DASHBOARD_JWT_SECRET=$(python -c "import secrets; print(secrets.token_urlsafe(32))")
export DASHBOARD_SECRET_KEY=$(python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())")
uvicorn main:app --reload --port 8099
Point PLATFORM_DB at the same platform.db the CLI uses if you want the consoles to show real
platform state; otherwise they render their empty states. Schema migrations for the dashboard's
own tables are Alembic's: cd platform/services/dashboard/backend && alembic upgrade head.
Backup¶
What to back up, in order of how much pain you're in if you lose it:
DASHBOARD_SECRET_KEY— without this, every encrypted row indashboard_configis unrecoverable.DASHBOARD_JWT_SECRET— if lost, every active JWT becomes invalid; users re-log in. Generate a new one and restart.- The Postgres
mlflowdatabase — containsdashboard_config(URLs + ciphertext) anddashboard_audit. The compose stack uses a named volume (postgres_data); back it up withdocker run --rm -v postgres_data:/data alpine tar czf - /data > backup.tar.gzor your usual Postgres dump. DASHBOARD_VIEWER_PASSWORD/DASHBOARD_ADMIN_PASSWORD— kept by whoever needs to log in.
Smoke checklist (run after any release)¶
- [ ]
make stack-upbrings the dashboard up;/api/healthreturns 200. - [ ] Login with viewer password →
role: viewerin JWT, Save button hidden on the Config page, "Read-only" banner visible. - [ ] Login with admin password → role chip says
admin, Audit link appears in side nav. - [ ] Edit
mlflow_url, hit Save → page reloads with the new value. - [ ] Set
grafana_api_key→ audit page shows asetrow. - [ ] Click the Clear (×) button on
grafana_api_key→ audit page shows aclearrow, GET returnsnullfor that key. - [ ]
docker compose restart dashboard→ secrets that were set survive (because they're in Postgres, not env). - [ ] Boot with
DASHBOARD_SECRET_KEYblank in.env→ dashboard exits non-zero with a clear error message;docker compose logs dashboardshows the validation failure.
Incident playbooks¶
Lost DASHBOARD_JWT_SECRET¶
Impact: every issued JWT is invalidated. Active sessions get 401 and re-prompt.
- Generate a new value:
python -c "import secrets; print(secrets.token_urlsafe(32))". - Update
.env. docker compose up -d dashboardto pick up the change.
No data loss. Users sign in again.
Lost DASHBOARD_SECRET_KEY¶
Impact: every row in dashboard_config with is_secret=true and a non-NULL
secret_value is unrecoverable.
- Update
.envwith a fresh key. docker compose up -d dashboard.- Connect to Postgres and clear the affected rows:
(The dashboard would otherwise raise InvalidToken on every read.)
4. As admin, re-enter the three secret values via the Config UI.
The audit log will record the new set rows.
Migrating from the legacy DASHBOARD_TOKEN¶
Pre-existing dashboards used a single DASHBOARD_TOKEN env var. After this
release:
- Replace
DASHBOARD_TOKENwith the four new vars from the Auth contract. - Generate
DASHBOARD_SECRET_KEY(see Bootstrap above). - Restart. The first boot creates the new tables and seeds the secret-key
rows; existing URL rows in
dashboard_configcarry over unchanged (is_secret=falseis the default).