Say how a site moves between stacks, in one place
The pieces existed across three pages and nothing connected them, so "can we go from IIS to Docker" had no answer to point at. It is a fair question with a short answer: the application keeps state in exactly two places, the database and the instance directory, and nothing is encrypted at rest with SECRET_KEY or JWT_SECRET_KEY, so a move is a dump plus a directory copy. The schema is identical across topologies. What the page spends its length on is the part that is NOT the data, because that is where the time goes. The server address is baked into things that are not the server: GE-Enforce manifests, the generated collector script, the printer client scripts, and printed QR codes, which cannot be swept at all. Keeping the hostname and repointing DNS makes the migration invisible to the fleet; changing it does not. Three other traps, each of which has a symptom that shows up later rather than at cutover: an aliased IIS site needs MOUNT_PATH and a dist built for that subpath, while the image builds for the root; an older dump can carry latin1 or 3-byte utf8 table definitions that load quietly into a utf8mb4 server and only misbehave on the first accented name; and a restored instance directory needs chown, because docker cp writes under the copying user's uid and the container runs as shopdb. Covers both directions plus Docker to a new Docker host, and ends with a cutover checklist that leaves the old stack stopped rather than removed until a bay has checked in on a working day. Also fixes two links in START-HERE that pointed at files which are not there: the ADR index needed its adr/ prefix, and LLM-GUIDE.md is llms.txt.
This commit is contained in:
214
docs/MIGRATE-TOPOLOGY.md
Normal file
214
docs/MIGRATE-TOPOLOGY.md
Normal file
@@ -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 <your-remote>/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:<password>@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
|
||||
Reference in New Issue
Block a user