Four gaps a second site hits and cannot resolve by reading. **Restoring on Windows** was one sentence - "the standard mysql < dump.sql" - with no ordering. Restoring a database under running code that expects a different schema turns a restore into a second incident, so the steps are now ordered and each says why. It also says what `.env` costs if it is lost, which is the part nobody discovers until they are already rebuilding: the dump does not contain it, and without the JWT secrets every issued token dies, so every collector and every GE-Enforce client on the fleet needs a new key. **Rolling back** had a paragraph saying downgrades are refused and a backup is the way back, but not the procedure. Rollback is restoring a matched pair, code and the schema it expects, in that order - and the doc now separates it from the case it gets confused with: a migration that failed mid-update has already been rolled back by the installer, and fixing forward is the only move. **Sizing, acquisition and support** were absent from the install guide entirely. A reader could not learn how big a server to ask for, where the .exe comes from, or where to raise a problem. The sizing is small and the reasons are stated, so a site does not over-provision a VM for a load that is a few dozen users. **Credentials** were described in three documents from three ends, so three answers existed for where a key lives. One table, both ends - server and PC - plus the two rules behind it: what a shop-floor PC holds is scoped to exactly what it does, and a credential is delivered rather than typed, because a value entered per machine is a value that is wrong on some machine.
8.1 KiB
Backup and Restore
Each site owns its own data (single-tenant, ADR-004), so backups are the site's responsibility. A complete backup is two parts:
- The MySQL database - all asset, user, audit, and settings data.
- The
instance/directory - uploaded floor plans, branding assets,plugins.json(the enabled-plugin list), and any tokens or files the app writes to disk. These are NOT in the database, so a DB-only backup loses them. Back upinstance/alongside every database dump.
Restoring the database without the matching instance/ directory leaves the
app pointing at floor plans and logos that no longer exist.
What to back up
| Item | Location | Why |
|---|---|---|
| Database | MySQL shopdb_flask |
All application data. |
instance/branding/ |
repo instance/ dir |
Uploaded logos and favicon. |
instance/modelimages/ |
repo instance/ dir |
Uploaded vendor-model photos. |
instance/employeephotos/ |
repo instance/ dir |
Uploaded self-hosted employee photos (external mode serves photos from the HR database instead). |
instance/ floor plans |
repo instance/ dir |
Uploaded map blueprints. |
instance/plugins.json |
repo instance/ dir |
Which plugins this site enabled. |
.env |
repo root (offline, secured) | Secrets needed to bring the stack back up. Store separately from the data backup, in a secrets manager. |
Backup
Database (Docker)
docker compose exec -T db mysqldump \
-u root -p"${MYSQL_ROOT_PASSWORD}" \
--single-transaction --routines --triggers \
shopdb_flask | gzip > shopdb-$(date +%F).sql.gz
--single-transaction gives a consistent dump without locking the tables (InnoDB).
Database (external MySQL, no container)
mysqldump -h <host> -u <user> -p \
--single-transaction --routines --triggers \
shopdb_flask | gzip > shopdb-$(date +%F).sql.gz
instance directory
tar czf instance-$(date +%F).tar.gz instance/
Recommended cadence: nightly database dump to offsite storage, 14-day
retention; instance/ captured on the same schedule (and always right before an
upgrade). Verify a restore quarterly.
Restore
Restoring replaces the current database contents. Do it into a known-empty or a throwaway target first if you are unsure.
Step 1: Bring up the stack (or a fresh one)
cp .env.example .env # or restore your saved .env
# ensure MYSQL_* and DATABASE_URL match the dump's database name (shopdb_flask)
docker compose up -d db
Wait for the db container to report healthy (docker compose ps).
Step 2: Load the database dump
gunzip -c shopdb-2026-07-10.sql.gz | \
docker compose exec -T db mysql -u root -p"${MYSQL_ROOT_PASSWORD}" shopdb_flask
For an external MySQL:
gunzip -c shopdb-2026-07-10.sql.gz | mysql -h <host> -u <user> -p shopdb_flask
If the target database does not exist yet, create it as utf8mb4 first (matching the schema charset):
CREATE DATABASE shopdb_flask CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
Step 3: Restore the instance directory
tar xzf instance-2026-07-10.tar.gz # restores ./instance/
The default docker-compose.yml does NOT bind-mount instance/ into the api
container (its only volume is - ./plugins:/app/plugins:ro, and the image never
copies instance/), so the container's Flask instance path is an empty
/app/instance and a restored host ./instance is invisible to it. To make the
restored instance/ visible, add a bind mount to the api service before starting
it:
api:
volumes:
- ./plugins:/app/plugins:ro
- ./instance:/app/instance
Make sure ./instance is present on the host before starting api.
Step 4: Bring up the API and reconcile migrations
docker compose up -d api
docker compose exec api flask db upgrade
docker compose exec api flask plugin upgrade-all
For a non-docker deploy:
flask db upgrade
flask plugin upgrade-all
flask db upgrade is a safety net: if the dump predates the current code, this
applies only the core Alembic chain. flask plugin upgrade-all then applies any
newer per-plugin migrations (each bundled plugin owns its own chain, ADR-008);
without it, plugin-owned tables stay un-migrated. If the dump is at the same
version both are no-ops.
Step 5: Verify
- Log in with a known account.
- Confirm the floor map renders (branding and map blueprints resolve from
instance/). - Spot-check a few asset records and the audit log.
curl -s -X POST -H "Content-Type: application/json" -d '{}' http://localhost:5001/api/auth/login | jq .should return aVALIDATION_ERROR, not a 500.
Windows sites (installer-built)
On a server installed from the Windows installer, everything above is wrapped by the operator console. Do not run mysqldump by hand:
cd C:\shopdb-flask
.\shopdb-admin.ps1 backup # C:\ProgramData\ShopDB-Flask\backups
.\shopdb-admin.ps1 backup D:\backups
The dump is verified complete before it is reported as good; a truncated one is deleted rather than left to be discovered when it is needed. An upgrade takes its own backup automatically before touching the schema, and restores from it if a migration fails.
Two Windows-specific notes:
- The backup directory is locked to Administrators and SYSTEM, because a dump contains every row including user password hashes. Keep it that way.
mysqldumpmust be present. It ships with the bundled-database option; a site using a remote MySQL needsmysqlclient\in its installer bundle, or the pre-upgrade backup is skipped.shopdb-admin.ps1 checkreports this.
Restoring on Windows, in order
Order matters. Restoring the database under running code that expects a different schema is how a restore turns into a second incident.
cd C:\shopdb-flask
# 1. Stop serving. The pool, not the whole site: other applications on this
# IIS server are unaffected.
.\shopdb-admin.ps1 stop
# 2. Restore the database. Use the dump taken closest BEFORE the problem,
# not the newest one - the newest may already contain it.
mysql -u root -p shopdb_flask < C:\ProgramData\ShopDB-Flask\backups\shopdb_flask-pre-upgrade-20260814-0730.sql
# 3. Restore instance\ if you are rebuilding a server rather than just
# reverting data. It holds uploaded branding, map blueprints, application
# images and warranty proofs - none of which are in the database.
robocopy D:\backups\instance C:\shopdb-flask\instance /MIR
# 4. Start, then prove it.
.\shopdb-admin.ps1 start
.\shopdb-admin.ps1 check -Json
check -Json reports version, publishing method, IIS and pool state, HTTP
reachability, database reachability, Python version and installed plugins. If it
passes, the restore worked; if the version it reports is not the version you
expect, see "Rolling back a release" in UPDATES-WINDOWS.md,
because a restored database and newer code is the one combination the installer
cannot fix for you.
What is lost if .env is lost
.env is not in the database dump, and rebuilding it is not simply retyping it:
| Value | If it is lost |
|---|---|
SECRET_KEY, JWT_SECRET_KEY |
Every issued token and session becomes invalid. Users log in again; managed API tokens must be reissued, which means every collector and GE-Enforce client needs its key replaced. Recoverable, but it is a fleet-wide job. |
DATABASE_URL password |
Recoverable: reset the MySQL user's password and write the new one in. |
MYSQL_ROOT_PASSWORD |
Recoverable through MySQL's own reset procedure, which requires stopping the server. |
ZABBIX_TOKEN and similar integration tokens |
Reissue at the far end. Nothing else breaks. |
So back it up with the database, to somewhere as protected as the dump - it is
ACL'd to Administrators and SYSTEM on the server for the same reason. A dump
without its .env restores the data and locks everyone out of it.
See OPERATE-WINDOWS.md.
See also
- DEPLOY.md - first-time deploy
- INSTALL-WINDOWS.md - Windows Server install
- UPGRADE.md - upgrade procedure (back up first)
- CONFIG.md - environment variables and Setting keys