Files
shopdb-flask/docs/MIGRATE-TOPOLOGY.md
cproudlock c6c806667e
Some checks failed
CI / backend (push) Failing after 7m18s
CI / naming (push) Failing after 7m14s
CI / frontend (push) Failing after 7m13s
CI / migrations-mysql (push) Failing after 7m10s
Run the database version the rest of the product already recommends
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.
2026-08-19 19:57:40 -04:00

8.2 KiB

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 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, INSTALL-WINDOWS-IIS and BACKUP-RESTORE. 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. 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:

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:

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:

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:

# 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) - 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 - what a complete backup contains, and the restore procedure each of these steps is built on
  • DEPLOY - the Docker stack, and the update procedure
  • DEPLOY-AIRGAP - the same stack where nothing can be pulled
  • INSTALL-WINDOWS-IIS - the Windows end in detail
  • UPGRADE - moving between product versions, which is a different question from moving between topologies
  • ADR-004 - why each site runs its own stack