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