Files
shopdb-flask/docs/DEPLOY.md
cproudlock d4e3ac9fc8 Phase 5: Alembic baseline, per-site deploy, ADRs to docs/adr
Migration runner ready and a sister site can deploy from a clean
checkout with one .env file.

ADRs relocated (migrations/adr/ -> docs/adr/):
- migrations/ is now Alembic territory, not docs.
- All cross-references updated: CLAUDE.md, docs/PLUGIN-HOOKS.md,
  docs/PLUGIN-QUICKSTART.md.

Alembic initialized (migrations/):
- env.py, script.py.mako, alembic.ini copied from Flask-Migrate
  templates so `flask db migrate` and `flask db upgrade` work without
  a one-time `flask db init` (which would clash with the existing
  migrations/ directory).
- Baseline migration generated via autogenerate, captures all 47
  tables (core models + 6 plugins) as the upgrade target. Ready for
  per-site `flask db upgrade` from an empty schema.

Deploy artifacts:
- Dockerfile: python:3.12-slim base, gunicorn server, non-root user,
  healthcheck against /api/auth/login. Single image bundles all six
  plugins; sites enable via `flask plugin install <name>`.
- docker-compose.yml: MySQL 8 + API container, healthcheck-gated
  startup, env-driven secrets that fail loud on missing values
  (`${SECRET_KEY:?}` form).
- .env.example: full env-var inventory with comments. Calls out
  required vs optional. Matches what ProductionConfig.validate
  enforces.

docs/DEPLOY.md:
- Step-by-step per-site runbook: clone, configure .env, bring up
  stack, run migrations, seed reference data, install plugins,
  create admin, front with TLS, backups, updates.
- Common-issues table.
- Cross-links to ADR-004 (per-site rationale), ADR-003 (plugin
  distribution), and the config source.

Skills:
- migrating-asset-schema: Alembic + one-shot data migration policy.
  Rules: additive first, renames are three steps, destructive ops
  need rollback, equipment migration filter per ADR-001 + ADR-005.
- hardening-flask-config: production validation, CORS allowlist
  policy, JWT cookie hardening, per-site deploy isolation per ADR-004.

CLAUDE.md updated to reflect the post-Phase-5 state. No tests added
this commit; the Alembic baseline is exercised by the existing
db.create_all-based test suite (tests do not touch the migration
runner; that's by design until per-plugin migrations land).

Test count unchanged: 101 passing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 17:56:19 -04:00

6.0 KiB

Per-Site Deployment Runbook

shopdb-flask is single-tenant per ADR-004. Each adopting facility runs its own stack: own DB, own users, own enabled plugins, own secrets. This document is the runbook for a fresh site deploy.

Prerequisites

  • Docker 24+ and Docker Compose v2 (or equivalent container runtime)
  • A reverse proxy with TLS termination (nginx, traefik, Caddy, GE corporate LB) -- the framework does not terminate TLS itself
  • A MySQL backup destination (offsite recommended)
  • Access to the GE Aerospace Gitea or a clone of the repo

Step 1: Clone and configure

git clone https://gitea.proudtech.net/ge-aerospace/shopdb-flask.git
cd shopdb-flask
cp .env.example .env

Edit .env:

Variable Required Notes
FLASK_ENV Yes production for live sites
SECRET_KEY Yes python -c "import secrets; print(secrets.token_urlsafe(64))"
JWT_SECRET_KEY Yes Same generation, different value
DATABASE_URL Yes mysql+pymysql://shopdb:PASSWORD@db:3306/shopdb_flask (matches docker-compose)
CORS_ORIGINS Yes Comma-separated explicit origins. Wildcard rejected.
MYSQL_ROOT_PASSWORD Yes Container only
MYSQL_PASSWORD Yes Container only, must match DATABASE_URL password
MYSQL_PORT No Default 3306
API_PORT No Default 5001
LOG_LEVEL No Default INFO
ZABBIX_URL, ZABBIX_TOKEN No Only if printers plugin uses Zabbix

Step 2: Bring up the stack

docker compose build
docker compose up -d

The MySQL container initializes its volume on first run. The API container waits for db to be healthy via healthcheck. Check logs:

docker compose logs -f api

If ProductionConfig.validate() raises, the container exits with the offending env-var named in the log. Fix .env and docker compose up -d again.

Step 3: Initialize the database schema

docker compose exec api flask db upgrade

This applies the baseline migration (creates all tables) and any subsequent migrations. Re-running is idempotent.

Step 4: Seed reference data

docker compose exec api flask seed reference-data

Creates: default Vendor, Location, BusinessUnit, OperatingSystem, AssetStatus, RelationshipType rows seeded with the platform contract values (partof, controls, connectedto).

Step 5: Pick plugins to enable

The image bundles all six plugins (computers, equipment, network, notifications, printers, usb). Only enabled plugins are loaded.

docker compose exec api flask plugin list
docker compose exec api flask plugin install computers
docker compose exec api flask plugin install equipment
# ... repeat for each plugin the site tracks

To install a sister-site or third-party plugin (per ADR-003), drop its directory into <repo>/plugins/<name>/ (the docker-compose mounts this read-only into the container) and run flask plugin install <name>.

Step 6: Create the admin user

docker compose exec api flask seed admin --username admin --email admin@facility.example.com
# Password is generated and printed once. Store in your password manager.

Subsequent users are managed through the UI.

Step 7: Front the API with TLS

The Flask container listens on 5001/tcp over plain HTTP. Production exposure must go through a reverse proxy that terminates TLS:

server {
  listen 443 ssl;
  server_name shopdb.facility-a.example.com;

  ssl_certificate     /etc/ssl/certs/shopdb.crt;
  ssl_certificate_key /etc/ssl/private/shopdb.key;

  location / {
    proxy_pass http://localhost:5001;
    proxy_set_header Host              $host;
    proxy_set_header X-Real-IP         $remote_addr;
    proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
  }
}

The framework reads X-Forwarded-For for audit logging.

Step 8: Backups

Per-site MySQL backups are the site's responsibility. Recommended: nightly mysqldump to offsite storage with 14-day retention.

docker compose exec -T db mysqldump -u root -p"${MYSQL_ROOT_PASSWORD}" shopdb_flask | gzip > backup-$(date +%F).sql.gz

Verify a restore quarterly.

Step 9: Updates

git pull origin main
docker compose build api
docker compose up -d api
docker compose exec api flask db upgrade

The framework's __contract_version__ may have moved. Check docs/adr/ for any new ADRs since the last update. If an ADR introduces a breaking change, the upgrade may require coordinated work; the ADR's "Consequences" section documents it.

Common issues

Symptom Cause Fix
ConfigError: SECRET_KEY is required in production .env missing or blank Set SECRET_KEY in .env, re-up
ConfigError: CORS_ORIGINS must be a comma-separated allowlist .env has * Set explicit origins
PluginVersionError: requires core_version X but framework is Y Plugin pinned a too-narrow range Update manifest.json core_version or pin framework version
500s after flask db upgrade Migration ran but app cached old schema docker compose restart api
Cannot reach API after restart Reverse proxy not pointing at the container's exposed port Confirm API_PORT and proxy config

Health check

curl -s -X POST -H "Content-Type: application/json" \
  -d '{}' http://localhost:5001/api/auth/login \
  | jq .
# Expect: {"status": "error", "data": {"error": {"code": "VALIDATION_ERROR", ...}}}

If this returns a 500 or no JSON, the container is unhealthy. Check docker compose logs api.

References