# 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
