docs: add VPS restore runbook
Explains why /web/database/manager restores appear to succeed but the live site keeps serving the old data (target DB name mismatch / Odoo not restarted / dbfilter pinned), and ships a 6-step SSH-based recovery procedure with safety-net backup, filestore handling, and verification checklist tied to the known QA fixtures. Made-with: Cursor
This commit is contained in:
183
docs/RESTORE_BACKUP_ON_VPS.md
Normal file
183
docs/RESTORE_BACKUP_ON_VPS.md
Normal file
@@ -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
|
||||||
|
<odoo_container> bash`) or wrap each `psql`/`odoo-bin` command with
|
||||||
|
`docker compose exec <service> …`.
|
||||||
|
|
||||||
|
## 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.
|
||||||
Reference in New Issue
Block a user