diff --git a/docs/RESTORE_BACKUP_ON_VPS.md b/docs/RESTORE_BACKUP_ON_VPS.md new file mode 100644 index 00000000..6623e48a --- /dev/null +++ b/docs/RESTORE_BACKUP_ON_VPS.md @@ -0,0 +1,183 @@ +# Restoring the local backup onto the production VPS + +The symptom you hit — restored a zip through `/web/database/manager` and +logged in to `https://encoach.com/` afterwards but the content looked +unchanged — almost always means one of three things: + +1. **The restore targeted a new database name**, so the zip loaded into + an orphan DB while the running app keeps serving the original one. +2. **The Odoo worker(s) were not restarted** after the restore, so the + in-memory registry is still the old one. +3. **`dbfilter` in `odoo.conf` pins the app to a specific DB name** that + doesn't match the one you restored into. + +This runbook walks through a clean restore. Run it on the VPS (SSH in); +if Odoo is dockerised, run inside the Odoo container (`docker exec -it + bash`) or wrap each `psql`/`odoo-bin` command with +`docker compose exec …`. + +## 0. Transfer the backup + +From your laptop: + +```bash +scp backups/encoach_v2_20260420_183426.zip user@encoach.com:/tmp/ +``` + +Zip contents (for reference): + +| File | Size | Purpose | +|---|---|---| +| `dump.sql` | 44 MB | plain pg_dump, no owner/ACL | +| `manifest.json` | 3 KB | Odoo 19.0, 94 modules, source DB `encoach_v2` | +| `filestore/` | 0 B | empty (no attachments yet) | + +## 1. Identify the *live* database name + +On the VPS: + +```bash +# If Odoo is systemd: +sudo grep -E '^(db_name|dbfilter)' /etc/odoo/odoo.conf + +# If Odoo is dockerised (compose): +docker compose exec backend sh -c "grep -E '^(db_name|dbfilter)' /etc/odoo/odoo.conf" +``` + +Note the value of `db_name` (e.g. `encoach_prod`). That is the name the +restored DB MUST use. If `dbfilter` is set too, it forces the same name. + +List what already exists in PostgreSQL: + +```bash +sudo -u postgres psql -c '\l' | grep -i encoach +``` + +## 2. Back up whatever is currently live (safety net) + +Before destroying anything: + +```bash +sudo -u postgres pg_dump --no-owner --no-privileges \ + -d encoach_prod \ + -f /tmp/encoach_prod_safety_$(date +%Y%m%d_%H%M%S).sql + +# Also snapshot the filestore (in case anyone uploaded attachments +# since the last clean snapshot) +sudo tar czf /tmp/filestore_prod_safety_$(date +%Y%m%d_%H%M%S).tgz \ + -C /var/lib/odoo/filestore . +``` + +## 3. Stop Odoo + +Nothing should be connected to the database during restore. + +```bash +# systemd: +sudo systemctl stop odoo + +# docker-compose: +docker compose stop backend +``` + +## 4. Drop & recreate the target DB, then load the dump + +Let's assume the live DB name is `encoach_prod`. Replace it with +whatever step 1 told you. + +```bash +cd /tmp +unzip -o encoach_v2_20260420_183426.zip -d encoach_restore + +sudo -u postgres psql <<'SQL' +-- Kick any lingering connections off the DB +SELECT pg_terminate_backend(pid) FROM pg_stat_activity + WHERE datname='encoach_prod' AND pid <> pg_backend_pid(); + +DROP DATABASE IF EXISTS encoach_prod; +CREATE DATABASE encoach_prod OWNER odoo ENCODING 'UTF8' LC_COLLATE='C' LC_CTYPE='C' TEMPLATE template0; +SQL + +sudo -u postgres psql -d encoach_prod -f /tmp/encoach_restore/dump.sql +``` + +Expected output: a long stream of `CREATE TABLE … COPY … ALTER TABLE …` +lines. If you see `ERROR:` entries, capture them — missing extensions +are the usual culprit (see §6). + +## 5. Restore the filestore & restart + +Our current backup has an empty filestore, so this step is a no-op for +this particular zip, but keep it in the runbook for future backups: + +```bash +# If your zip has filestore entries: +sudo rm -rf /var/lib/odoo/filestore/encoach_prod +sudo mkdir -p /var/lib/odoo/filestore/encoach_prod +sudo cp -a /tmp/encoach_restore/filestore/. /var/lib/odoo/filestore/encoach_prod/ +sudo chown -R odoo:odoo /var/lib/odoo/filestore/encoach_prod + +# Start Odoo again +sudo systemctl start odoo # systemd +docker compose start backend # docker-compose +``` + +Then watch the log to confirm the registry loaded cleanly: + +```bash +sudo journalctl -u odoo -f # systemd +docker compose logs -f backend # docker +``` + +Look for `Registry loaded` and no `Traceback` near the end. + +## 6. Verify + +Open `https://encoach.com/` in a private / incognito window (to bypass +any service-worker or session cache), log in as `admin` / `admin`, and +check: + +| Where | Expect | +|---|---| +| Sidebar → Exams List | 18 custom exams, including **"QA Smoke Writing"** | +| Sidebar → Rubrics | **"QA Writing Rubric"** visible | +| Sidebar → Exam Structures | **"QA IELTS Academic"** visible | +| Sidebar → Assignments | **"QA Smoke Assignment v2"** visible | + +If any of the "QA …" items are missing, the restore didn't actually hit +this DB — repeat from step 1 with the correct db_name. + +## If `psql -f dump.sql` errors out + +Common errors and fixes: + +- **`extension "unaccent" is not available`** — install it on the VPS: + ```bash + sudo -u postgres psql -d encoach_prod -c "CREATE EXTENSION IF NOT EXISTS unaccent;" + ``` + Then rerun step 4's dump load. + +- **`role "odoo" does not exist`** — the dump is `--no-owner` / `--no-acl` + so this shouldn't happen, but if it does, create the role: + ```bash + sudo -u postgres psql -c "CREATE USER odoo WITH PASSWORD 'change-me';" + ``` + +- **`server_version` mismatch warning** — the dump was made on PG 18.3, + safe to restore on PG 14/15/16/18. If you see `incompatible dump + format`, the target PG is too old — upgrade or use `pg_restore -F c` + from a custom-format dump instead. + +## Why NOT to use `/web/database/manager` on a production VPS + +- It requires `list_db = True` in odoo.conf, which exposes a + "Select Database" dropdown to anonymous visitors — a tenant-leak + footgun. +- The UI silently restores into whatever DB name you type, even if it + doesn't match what `dbfilter` is set to — which is exactly how you + got into this state. +- For dockerised deployments, the filestore winds up inside the + container but the zip's filestore isn't restored in the right volume. + +Keep `list_db = False` in production and always restore via the steps +above.