# 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 ```bash 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 ```bash 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: ```bash 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 ```bash 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 ```bash 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. ```bash 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 `/plugins//` (the docker-compose mounts this read-only into the container) and run `flask plugin install `. ## Step 6: Create the admin user ```bash 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: ```nginx 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. ```bash 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 ```bash 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 ```bash 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 - [docs/adr/ADR-004-deployment-topology.md](adr/ADR-004-deployment-topology.md) - per-site instances rationale - [docs/adr/ADR-003-plugin-distribution.md](adr/ADR-003-plugin-distribution.md) - bundled vs external plugins - [docs/adr/ADR-006-collector-contract.md](adr/ADR-006-collector-contract.md) - per-plugin collector endpoints - [docs/PLUGIN-QUICKSTART.md](PLUGIN-QUICKSTART.md) - building a custom plugin for your site - [shopdb/config.py](../shopdb/config.py) - all the env-vars in one place