diff --git a/docs/MIGRATE-TOPOLOGY.md b/docs/MIGRATE-TOPOLOGY.md new file mode 100644 index 0000000..b59ec60 --- /dev/null +++ b/docs/MIGRATE-TOPOLOGY.md @@ -0,0 +1,214 @@ +# Moving a site between deployment topologies + +Windows/IIS to Docker, Docker to a new Docker host, or Docker back to Windows. +All three are the same job, because the application keeps state in exactly two +places: + +1. **The MySQL database** - every asset, user, audit and settings row. +2. **The instance directory** - `plugins.json` plus every uploaded file. Not in + the database. See [BACKUP-RESTORE](BACKUP-RESTORE.md) for the full inventory. + +There is no third store. Nothing is encrypted at rest with `SECRET_KEY` or +`JWT_SECRET_KEY` (the only cryptography in the product is Ed25519 plugin +signing, which carries its own keys), so a move is a database dump plus a +directory copy. The schema is identical across topologies; only the environment +around it changes. + +This page is the connective tissue between [DEPLOY](DEPLOY.md), +[INSTALL-WINDOWS-IIS](INSTALL-WINDOWS-IIS.md) and +[BACKUP-RESTORE](BACKUP-RESTORE.md). Read those for the details of each end. + +--- + +## Before you start: what actually makes this hard + +The data moves cleanly. These four things are the work. + +### 1. The fleet points at the old URL + +This is the item that turns a one-hour job into a project. The server address is +baked into things that are not the server: + +| Where | What carries the URL | +|---|---| +| GE-Enforce | manifest entries, and the client's configured server | +| Computers collector | the generated reporter script (Settings > Computers) | +| Printers | the client scripts under `plugins/printers/client/` | +| Printed labels | QR codes, which point at asset pages | +| Browsers | bookmarks, and any kiosk or display configured with a URL | + +**Keep the hostname and repoint DNS at the new host** wherever you can. The +migration then costs nothing on the fleet side. If the hostname must change, +budget for a sweep of every row in that table, and note that printed QR codes +cannot be swept at all: they are reprinted or redirected. + +### 2. The subpath may differ + +An IIS install can serve under an alias (`/ops`, say), which requires +`MOUNT_PATH` on the backend and a `frontend/dist` built with a matching +`VITE_BASE_PATH` - see step 7b of [INSTALL-WINDOWS-IIS](INSTALL-WINDOWS-IIS.md). +The Docker image builds `dist` for the root path. + +So moving an aliased IIS site to Docker changes the URL even if the hostname +stays. Either serve Docker at the root and accept the path change (then item 1 +applies), or put a reverse proxy in front that preserves the alias and build the +image with the matching `VITE_BASE_PATH`. + +### 3. MySQL version and character set + +The Windows runbook supports 5.6, 5.7 and 8.4. `docker-compose.yml` runs +`mysql:8.0` and forces `utf8mb4` / `utf8mb4_unicode_ci` server-wide so every +site shares one collation. + +A dump from an older server can carry `latin1` or 3-byte `utf8` table +definitions. Those load without complaint and leave you on a mixed-charset +schema that only misbehaves later, on a name with an accent in it. Dump with +`--default-character-set=utf8mb4` and grep the SQL for `CHARSET=` before loading +anything. + +Going 5.6 or 5.7 forward to 8.x is a supported upgrade path. Going backward is +not: an 8.x dump can use syntax an older server rejects. + +### 4. File ownership + +On Windows the IIS app pool holds Modify on `APP_ROOT\instance`. In the +container the application runs as `shopdb` (uid 1000). `docker compose cp` +writes files under the *copying* user's numeric uid, so the restored tree needs +an explicit `chown` or the site will read its files and fail to write new ones. + +--- + +## Docker to a new Docker host + +The simple case. Same image, same layout, same paths. + +On the OLD host: + +```bash +docker compose exec -T db mysqldump -u root -p"${MYSQL_ROOT_PASSWORD}" \ + --single-transaction --routines --triggers --default-character-set=utf8mb4 \ + shopdb_flask | gzip > shopdb-$(date +%F).sql.gz + +docker compose cp api:/app/instance ./instance-export +cp .env ./env-export +``` + +On the NEW host: + +```bash +git clone /shopdb-flask.git && cd shopdb-flask +cp /path/to/env-export .env +# Edit .env: CORS_ORIGINS for the new hostname. Keep SECRET_KEY and +# JWT_SECRET_KEY as they were - regenerating only forces everyone to log in +# again, and buys nothing. + +docker compose up -d db +# Wait for the healthcheck to pass, then load the dump: +zcat shopdb-*.sql.gz | docker compose exec -T db \ + mysql -u root -p"${MYSQL_ROOT_PASSWORD}" --default-character-set=utf8mb4 shopdb_flask + +docker compose build api +docker compose up -d api + +docker compose cp ./instance-export/. api:/app/instance +docker compose exec -u root api chown -R shopdb:shopdb /app/instance +docker compose restart api + +docker compose exec api flask db upgrade +docker compose exec api flask plugin upgrade-all +``` + +Verify before you decommission the old host: + +```bash +docker compose exec api flask db current # must match `flask db heads` +docker compose exec api flask db heads +docker compose exec api flask plugin list # the site's plugins, enabled +``` + +Then log in and open a page with a floor map and one with images. The database +looks correct whether or not the files came across, so this is the only check +that proves the instance directory landed. + +--- + +## Windows/IIS to Docker + +Same shape. The dump comes from a normal MySQL server rather than a container, +and the instance directory is a normal folder. + +On the WINDOWS host: + +```powershell +# 1. Stop serving, so the dump and the file copy agree with each other. +Stop-WebAppPool -Name shopdbflask + +# 2. Database. +mysqldump -u root -p --single-transaction --routines --triggers ` + --default-character-set=utf8mb4 shopdb_flask | ` + Out-File -Encoding utf8 shopdb-export.sql + +# 3. Instance directory and environment. +Compress-Archive -Path APP_ROOT\instance\* -DestinationPath instance-export.zip +Copy-Item APP_ROOT\.env .\env-export +``` + +Move all three to the Docker host, unzip the instance archive into +`./instance-export/`, then follow the "NEW host" block above with two changes: + +- `.env` needs `DATABASE_URL` rewritten to point at the `db` service rather than + the Windows MySQL server: + `mysql+pymysql://shopdb:@db:3306/shopdb_flask?charset=utf8mb4` +- `MOUNT_PATH` comes out unless you are preserving a subpath (item 2 above). + +Leave the Windows site installed but stopped until the Docker site is verified. +Rolling back is then a matter of starting the app pool again. + +### What does not need migrating + +`plugins.json` comes across in the instance directory, so the site's enabled +plugin set follows it. The image bakes the whole catalog, so whatever was +enabled on Windows is available in the container. The schema is identical, so +`db upgrade` and `plugin upgrade-all` are no-ops unless the target is also a +newer release. + +--- + +## Docker to Windows/IIS + +The reverse works the same way and is worth knowing for a rollback. Dump from +the container, restore into the Windows MySQL server, unpack the instance +directory into `APP_ROOT\instance`, and grant the app pool Modify on it (step +7.3 of [INSTALL-WINDOWS-IIS](INSTALL-WINDOWS-IIS.md)) - the container's uid +means nothing to Windows, and a directory the pool cannot write produces +"internal error" on any upload or plugin toggle. + +The one constraint is MySQL version: do not restore an 8.x dump into a 5.6 or +5.7 server. + +--- + +## Cutover checklist + +1. Announce the outage. The database is stopped for the dump. +2. Take the dump and the instance copy from the SAME quiet moment. +3. Stand the new stack up and restore both. +4. Run `db upgrade` and `plugin upgrade-all`, then `db current` against + `db heads`. +5. Log in. Load a floor map. Load a page with images. Toggle nothing. +6. Repoint DNS, or update the fleet's server address if the hostname changed. +7. Confirm one bay checks in and one collector report arrives. +8. Leave the old stack in place, stopped, until step 7 has happened at least + once on a working day. + +## See also + +- [BACKUP-RESTORE](BACKUP-RESTORE.md) - what a complete backup contains, and the + restore procedure each of these steps is built on +- [DEPLOY](DEPLOY.md) - the Docker stack, and the update procedure +- [DEPLOY-AIRGAP](DEPLOY-AIRGAP.md) - the same stack where nothing can be pulled +- [INSTALL-WINDOWS-IIS](INSTALL-WINDOWS-IIS.md) - the Windows end in detail +- [UPGRADE](UPGRADE.md) - moving between product versions, which is a different + question from moving between topologies +- [ADR-004](adr/ADR-004-deployment-topology.md) - why each site runs its own + stack diff --git a/docs/START-HERE.md b/docs/START-HERE.md index c354e43..c2a8900 100644 --- a/docs/START-HERE.md +++ b/docs/START-HERE.md @@ -43,7 +43,7 @@ artifact comes from. ## I am integrating with the API -1. [LLM-GUIDE](LLM-GUIDE.md) (`docs/llms.txt` in the repo, also served at +1. [LLM-GUIDE](llms.txt) (`docs/llms.txt` in the repo, also served at `/api/docs/llms.txt`) - auth, the response envelope, common recipes. Short, and the envelope section is the part people get wrong. 2. `GET /api/docs` on any running instance - the full spec, browsable. @@ -52,7 +52,7 @@ artifact comes from. ## I am trying to understand why something is built this way -[The ADRs](ADR-001-asset-as-platform-contract.md). They are the decision record, +[The ADRs](adr/ADR-001-asset-as-platform-contract.md). They are the decision record, they say what was rejected and why, and they are the fastest way to avoid relitigating a settled question. [PROJECT-MAP](PROJECT-MAP.md) lists them all with their status, along with the current versions and every migration head - @@ -65,3 +65,8 @@ it is generated, so it is never stale. [FLEET-ARCHITECTURE](FLEET-ARCHITECTURE.md) says which piece to open first. - A deploy that half-worked: [UPGRADE](UPGRADE.md) and [BACKUP-RESTORE](BACKUP-RESTORE.md). + +## I am moving a site to a different server or stack + +[MIGRATE-TOPOLOGY](MIGRATE-TOPOLOGY.md) - Windows/IIS to Docker, Docker to a new +host, and back. The data moves cleanly; the URL the fleet points at is the work.