Files
shopdb-flask/docs/BACKUP-RESTORE.md
cproudlock e005d1846a docs: wiki staleness sweep (Fable-orchestrated Opus audit)
Audited all 40 docs/ against the live codebase; fixed factual staleness in 23,
14 were clean. Highlights (all verified against code):
- equipment -> machines (ADR-011 rename) in INSTALL/DEPLOY-WINDOWS-IIS,
  PLUGIN-GUIDE, GE-ENFORCE, ROADMAP.
- Versions refreshed: contract 0.10.0 -> 0.13.0, product 0.5.0 -> 0.7.0, plus
  plugin example core_version pins.
- Bundled set corrected to the current 13 (PLUGINS.md 7 -> 13 rows; DEPLOY
  eleven -> thirteen).
- Per-plugin Alembic chain workflow (ADR-008) replacing stale core-chain steps
  in PLUGIN-QUICKSTART / BACKUP-RESTORE; deploy adds plugin upgrade-all.
- Frontend plugin staging (ADR-010) replacing 'no frontend plugin system yet'
  in PLUGIN-GUIDE; view/route paths repointed to plugins/<name>/frontend/.
- Corrected file paths (MapView.vue, manifest_schema.json), CLI (shelf-list),
  API gating (GET /api/plugins is optional-jwt), WJF 15 -> 16 stages, and
  retired Collector/PC-Types settings pages (ADR-012).
- ge-enforce proposal marked ACCEPTED/built.
2026-07-19 12:54:53 -04:00

4.8 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:

  1. The MySQL database - all asset, user, audit, and settings data.
  2. 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 up instance/ 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 a VALIDATION_ERROR, not a 500.

See also