Files
shopdb-flask/docs/DEPLOY.md
cproudlock b8c22244a1
Some checks failed
CI / backend (push) Failing after 2s
CI / naming (push) Successful in 1s
CI / frontend (push) Successful in 7s
Multi-site distribution readiness: settings-driven site config, security closeout, release engineering, v0.5.0
Make the app distributable to other GE Aerospace sites (one self-hosted
instance per site, ADR-004). GE values remain the shipped defaults; every
site-specific behavior is now a Setting an admin can change in the UI.

Settings-driven site config:
- Branding: site/QR/badge logos, favicon, primary color (upload endpoints
  mirror the map-blueprint pattern; new Settings > Branding section).
- ServiceNow: search/incident/change URL templates ({ticket}), ticket
  prefixes, enable toggle. Defaults point at the current
  geaerospaceqa.service-now.com global search. Disabled = plain-text tickets.
- Employee-id regex (employeeid_pattern), printer hostname template,
  QR label targets (qr_target_printer / qr_target_usb, blank = asset page,
  else URL template with placeholders), usb_label_style (barcode|qr).
- West Jefferson floor-plan PNGs removed from the tree; generic placeholder
  ships as the map default and sites upload their own blueprint.

Security closeout:
- dashboarddefaults writes now require admin.
- Collector: generic error messages (no str(exc) leak); API key accepted
  via X-API-Key header only (BREAKING: querystring api_key removed).
- IP-based login rate limiting (AUTH_RATELIMIT_* knobs) atop account lockout.
- Setting.set() creation race fixed (IntegrityError retry).

Release engineering and docs:
- __version__ 0.5.0 (distinct from __contract_version__, ADR-007),
  CHANGELOG.md, Gitea Actions CI config, frontend version aligned.
- One wizard-first install story across README/DEPLOY; new CONFIG.md,
  UPGRADE.md, BACKUP-RESTORE.md; CLAUDE.md and ROADMAP de-staled.
- Dockerfile multi-stage build now bundles the frontend; compose binds
  MySQL to 127.0.0.1; stale database/schema.sql and one-off SQL removed.

Debt and fixes:
- .query.get() -> db.session.get() sweep; datetime.utcnow() removed
  (naive-UTC via timezone-aware now); users.py on authz decorators.
- Fixed 4 stale tests (slides feed shape, shopfloor splitperemployee,
  plugin contract purity) and the USB label page field mapping (both usb
  modes emit the cmmc shape: device_id/device_desc).
- Health endpoint reports the real version.

248 tests pass; naming/style check green; frontend builds; fresh-DB
flask db upgrade + seeds verified; QR targets verified by decoding
rendered codes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 15:02:07 -04:00

9.4 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
COLLECTOR_API_KEY No Shared key for /api/collector/* ingest. Required only if unattended collectors push data. Endpoint fails closed (denies) when unset.
COLLECTOR_API_KEY_<PLUGIN> No Per-plugin override (e.g. COLLECTOR_API_KEY_COMPUTERS), checked before the shared key (ADR-006)
EMPLOYEE_DB_HOST/USER/PASSWORD/NAME No Read-only HR directory for notifications + kiosks. No safe default for the password.

Step 2: Bring up the stack

docker compose build
docker compose up -d

The Docker image builds the Vue frontend in a first stage and copies the compiled SPA into the API image, so docker compose build produces a self-contained image with the UI already built. No separate Node step is needed for a container deploy. (For a bare-metal/venv install instead, build the frontend by hand: cd frontend && npm ci && npm run build, which writes frontend/dist/ for Flask to serve.)

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.

Charset: the schema is utf8mb4 (utf8mb4_unicode_ci). The docker-compose db service sets --character-set-server=utf8mb4, so the auto-created shopdb_flask database is utf8mb4. If you point at an external MySQL instead of the bundled container, create the database as utf8mb4 first, or it inherits the server default (often latin1) and the schema silently drifts:

CREATE DATABASE shopdb_flask CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

DATABASE_URL must keep ?charset=utf8mb4 so the connection matches. On MySQL older than 5.7 also enable innodb_large_prefix=ON + innodb_file_format=Barracuda, or the utf8mb4 indexes exceed the 767-byte prefix limit (error 1071). MySQL 5.7+ and 8.0 need no extra config.

Step 4: Seed permissions, settings, and reference data

Run all three seeders. They are idempotent, so re-running them on an existing database is safe (it adds anything missing and leaves existing rows alone).

docker compose exec api flask seed permissions
docker compose exec api flask seed settings
docker compose exec api flask seed reference-data
  • seed permissions - creates the RBAC permission rows and the default roles the app checks with @require_permission. Run this before anyone logs in or permission checks have nothing to match.
  • seed settings - writes the default Setting rows (branding, ServiceNow integration, floor-map placeholders, search toggles, site identity). A site overrides these later in Settings or the setup wizard.
  • 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 ten plugins (computers, employees, equipment, knowledgebase, network, notifications, printers, slides, usb, warranty). 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 first admin (setup wizard)

The primary path is the first-run setup wizard. Once the stack is up and the DB is seeded, browse to the site (through the reverse proxy configured in Step 7, or directly at the API port during bring-up) and go to /setup. The wizard creates the first admin account and captures site identity (facility name, optional logo). It runs only while setup_complete is false; after it finishes the route redirects to the app.

Headless alternative (no browser, e.g. automated provisioning):

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. Back up the instance/ directory alongside the DB; it holds uploaded floor plans, branding, plugins.json, and tokens that are not in MySQL. See docs/BACKUP-RESTORE.md for the full backup and restore procedure.

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. See docs/UPGRADE.md for the full upgrade procedure, including re-seeding and the v0.5+ floor-plan note.

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