1
BACKUP RESTORE
Cameron Proudlock edited this page 2026-07-13 19:51:21 -04:00

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 docker-compose api container reads instance/ from the repo working directory; make sure it is present before starting api.

Step 4: Bring up the API and reconcile migrations

docker compose up -d api
docker compose exec api flask db upgrade

flask db upgrade is a safety net: if the dump predates the current code, this applies any newer migrations. If the dump is at the same version it is a no-op.

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