INSTALL-WINDOWS-IIS.md has said MySQL 8.4 LTS is standard for new installs since 8.0 reached end of life in April 2026, while both compose files and the offline bundler still pinned 8.0. A site reading the Windows runbook and a site reading the Docker one were being told to run different servers, and the migration page written this week sent people onto the dead one. Verified against a real server rather than by editing a tag: 8.4.11, core chain plus five plugin chains applied clean, 66 tables at a single utf8mb4_unicode_ci collation, six alembic version tables. The image's PyMySQL authenticates against 8.4's caching_sha2_password, which is what requirements.in already pins cryptography for. Existing servers need one thing done FIRST: 8.4 removes mysql_native_password, so an account created on 5.6 or 5.7 must be moved to caching_sha2_password before the upgrade or it cannot authenticate afterwards. In-place also has no downgrade path, and 5.7 cannot reach 8.4 in one hop. For databases this size a dump into a fresh 8.4 server is the better trade: same outage, and the old server stays as the rollback. Air-gapped sites need a fresh offline bundle, because the tarball carries the MySQL image alongside the app image. Also here, found by having it bite during that verification: the db healthcheck pinged over the unix socket, and the entrypoint's init pass answers on the socket while running the server with --skip-networking. The probe therefore reported healthy DURING init, which is what `depends_on: service_healthy` gates api and migrate on. A ping passed at 8 seconds and the next query failed because the server was mid-restart. Probing 127.0.0.1 keeps it red until the real server is listening.
215 lines
8.2 KiB
Markdown
215 lines
8.2 KiB
Markdown
# 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.4` 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
|