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>
This commit is contained in:
161
docs/DEPLOY.md
Normal file
161
docs/DEPLOY.md
Normal file
@@ -0,0 +1,161 @@
|
||||
# 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 `<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
|
||||
|
||||
```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
|
||||
Reference in New Issue
Block a user