Live off-site Postgres copy on 247 replaces the nightly pg_dump; backup retention in deploy.sh

- ops/pgbackup: SSH tunnel + pg_receivewal (slot zareian_247) + daily verified pg_basebackup (keep 7)
  and a restore check, all run on 78.38.35.247 as systemd units
- /api/health reports offsiteBackup (slot active/lag/lost) and warns; OFFSITE_WAL_SLOT setting
- removed statuslog backup_db/backup_verify and their health warnings
- deploy.sh keeps 3 pre-deploy dumps, 3 local patches, drops manual backups after 30 days
- docs: operations backup section, security item 14 closed, CLAUDE.md status

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-10 16:53:10 +03:30
parent 19072ee534
commit a9cccc6550
22 changed files with 374 additions and 447 deletions

View File

@@ -72,25 +72,73 @@ curl -i https://mathapi.tadrisino2.ir/api/health
## بکاپ و بازیابی
دیتابیسِ سرور Postgres است (cluster جدای `zareian`، پورت ۵۴۴۰، فقط localhost؛ راهنمای کامل: `گزارش-۲۹/راهنمای-Postgres-و-Redis.html`). سه لایه:
دیتابیسِ سرور Postgres است (cluster جدای `zareian`، پورت ۵۴۴۰، فقط localhost؛ راهنمای کامل: `گزارش-۲۹/راهنمای-Postgres-و-Redis.html`).
| لایه | چه زمانی | نگه‌داری | کجا |
| لایه | چه چیزی | از دست رفتنِ داده در بدترین حالت | کجا |
|---|---|---|---|
| پیش از migrate | هر `./deploy.sh` (`pg_dump -Fc`؛ اگر نشد migrate اجرا نمی‌شود) | ۲ تای آخر | `.deploy-backups/db-before-deploy-*.pgdump` |
| روزانه | cron: `python manage.py backup_db` (`statuslog/backup.py`) | ۷ روزِ آخر + ۴ هفته‌ی آخر | `.deploy-backups/scheduled/` + `status.json` |
| بیرونِ ماشین | همان فرمان، اگر `BACKUP_OFFSITE_CMD` در `.env` باشد (فرمانی با `{file}`، مثلاً rsync/scp/rclone) | دست‌ِ مقصد | مقصد را ادمین تعیین می‌کند؛ تا تعیین نشده `/api/health` هشدار می‌دهد |
| کپیِ زنده (اصلی) | هر تغییرِ دیتابیس لحظه‌ای به سرورِ دیگر می‌رود + یک نسخه‌ی کامل در روز؛ بازیابی تا هر لحظه از ۷ روزِ اخیر | چند ثانیه‌ی آخر | `administrator@78.38.35.247:~/zareian-pgbackup/` |
| پیش از migrate | `./deploy.sh` (`pg_dump -Fc`؛ اگر نشد migrate اجرا نمی‌شود)؛ برای برگشتن از migrationِ خراب | — | `~/mathapp-v2/.deploy-backups/db-before-deploy-*.pgdump` (۲ تای آخر) |
| ایمیجِ کلِ ماشین | کارِ ادمینِ سرور، روزانه؛ ما محتوایش را نسنجیده‌ایم | — | دستِ ادمین |
نصبِ بکاپِ شبانه (یک بار، با `crontab -e` حسابِ `administrator`؛ بکاپِ crontab را پیش از ویرایش بگیرید):
بکاپِ شبانه‌ی جداگانه (`backup_db` با cron) حذف شد: کپیِ زنده همان کار را بهتر می‌کند و ایمیجِ روزانه را ادمین دارد.
### کپیِ زنده چطور کار می‌کند
Postgres هر تغییر را اول در یک دفترِ پیوسته (WAL) می‌نویسد. سرورِ ۲۴۷ این دفتر را با ابزارِ خودِ Postgres
(`pg_receivewal`) لحظه‌ای می‌گیرد، نه کپیِ کلِ دیتابیس: فقط تغییرات می‌روند. روزی یک بار هم یک نسخه‌ی کامل
(`pg_basebackup`) گرفته می‌شود؛ ۷ نسخه‌ی آخر می‌مانند و WALِ قدیمی‌تر از قدیمی‌ترینشان پاک می‌شود، پس حجم
محدود است. بازیابی = آخرین نسخه‌ی کاملِ پیش از لحظه‌ی دلخواه + بازپخشِ WAL تا آن لحظه.
```
15 2 * * * cd /home/administrator/mathapp-v2 && venv/bin/python manage.py backup_db >> .deploy-backups/scheduled/cron.log 2>&1
۲۴۹ (اپ) ۲۴۷ (بکاپ)
Postgres 5440 (localhost) ◄── تونلِ SSH ─── zareian-pg-tunnel (127.0.0.1:15440)
اسلاتِ zareian_247 zareian-pg-wal → wal/ (لحظه‌ای)
نقشِ zareian_replicator zareian-pg-basebackup.timer → base/ (هر روز ۰۴:۳۰، ۷ تا)
```
**آزمونِ بازیابی (ماهی یک بار، و پس از هر تغییرِ بکاپ):** `venv/bin/python manage.py backup_verify` آخرین بکاپ را روی دیتابیسِ آزمایشیِ `<DB_NAME>_restorecheck` برمی‌گرداند، شمارشِ کاربر/ورود/سفارش/تراکنش/لاگِ وضعیت را با لحظه‌ی بکاپ می‌سنجد و آن دیتابیس را می‌اندازد؛ به دیتابیسِ اصلی دست نمی‌زند. نقشِ برنامه باید `CREATEDB` داشته باشد (`ALTER ROLE zareian_app CREATEDB;` با ادمینِ Postgres). فایلِ دست‌خورده را با هش رد می‌کند.
- **۲۴۹ را نمی‌خواباند:** اپ منتظرِ ۲۴۷ نمی‌ماند (تکثیرِ ناهمزمان). اگر ۲۴۷ قطع شود، اسلات تغییرات را نگه می‌دارد تا
وصل شود، ولی حداکثر ۱ گیگ (`max_slot_wal_keep_size`)؛ بیش از آن اسلات «lost» می‌شود و دیسکِ ۲۴۹ امن می‌ماند.
نسخه‌ی کامل با `--checkpoint=spread --max-rate=20M` گرفته می‌شود.
- **امنیت:** کلیدِ SSHِ ۲۴۷ در `authorized_keys`ِ ۲۴۹ با `restrict,port-forwarding,permitopen="127.0.0.1:5440"`
محدود است (نه شِل، نه پورتِ دیگر). رمزِ نقشِ `zareian_replicator` فقط در `~/.pgpass`ِ ۲۴۷ است. این نقش
همه‌ی داده را می‌خواند (طبیعتِ بکاپ)، پس `~/zareian-pgbackup` با مجوز ۷۰۰ است. ترافیک در شبکه‌ی داخلی است
(۲۴۹ = `192.168.200.164`، ۲۴۷ = `192.168.200.167`).
- **Postgresِ ۲۴۷ را به کار نمی‌گیریم:** سرویسِ `postgresql@14-main` آن‌جا مالِ دیگران است. ما فقط ابزارهای
Postgres 14 را اجرا می‌کنیم؛ آزمونِ بازیابی یک Postgresِ موقت روی سوکتِ داخلِ پوشه‌ی خودمان بالا می‌آورد.
- **پایش:** `/api/health` (با `X-Internal-Key`) زیرِ `offsiteBackup` وضعیت و عقب‌ماندگی را می‌دهد و اگر ۲۴۷
وصل نباشد، عقب باشد یا اسلات lost شده باشد هشدار می‌دهد (`OFFSITE_WAL_SLOT=zareian_247` در `.env`ِ ۲۴۹).
شکستِ نسخه‌ی کاملِ روزانه از ۲۴۹ دیده نمی‌شود: روی ۲۴۷ `systemctl status zareian-pg-basebackup` و
`journalctl -u zareian-pg-basebackup`. شکستش داده را از دست نمی‌دهد (WAL از آخرین نسخه‌ی سالم نگه داشته
می‌شود)، فقط بازیابی را کندتر می‌کند.
**بازیابیِ واقعی** (دیتابیسِ اصلی خراب شد): `sudo systemctl stop zareian-mathapp` ← از دیتابیسِ فعلی یک `pg_dump` دیگر بگیرید ← `dropdb`/`createdb` ← `pg_restore --no-owner -d zareian_mathapp <فایل>` ← `start` ← `/api/health`. سفارش‌های بعد از زمانِ بکاپ را با پنلِ زرین‌پال/بازار تطبیق دهید.
فایل‌ها در `ops/pgbackup/` ریپو. روی ۲۴۷: یونیت‌ها در `/etc/systemd/system/zareian-pg-*`، اسکریپت‌ها در
`~/zareian-pgbackup/bin/`. هر دو با دست کپی می‌شوند (`deploy.sh` فقط ۲۴۹ را می‌شناسد)؛ بعد از تغییرِ
یکی از آن‌ها: کپی، `sudo systemctl daemon-reload`، و `restart` همان یونیت.
آزموده‌شده: بکاپ، نگه‌داری، بازیابی و رد‌شدنِ فایلِ خراب روی Postgresِ لپ‌تاپ. آزموده‌نشده: زیر cron روی سرور، دیسکِ نزدیک به پُر، `BACKUP_OFFSITE_CMD` واقعی.
### آزمونِ بازیابی (ماهی یک بار)
روی ۲۴۷: `~/zareian-pgbackup/bin/restore-check.sh` (یا با یک لحظه: `… '2026-10-10 16:30:00+03:30'`).
آخرین نسخه‌ی کامل + همه‌ی WAL را در پوشه‌ی موقت بالا می‌آورد، شمارش و آخرین زمانِ کاربر/ورود/سفارش/تراکنش/
تغییرِ وضعیت را چاپ می‌کند و پاک می‌کند. همان پرسش را روی ۲۴۹ بزنید؛ باید یکی باشند (جز تغییرِ همان چند ثانیه).
### اگر ۲۴۷ مدتی قطع بود
`/api/health` می‌گوید `disconnected`. با برگشتنِ ۲۴۷ سرویس‌ها خودشان وصل می‌شوند و عقب‌ماندگی را می‌گیرند. اگر
`lost` شد (بیش از ۱ گیگ تغییر در قطعی): روی ۲۴۷
`systemctl stop zareian-pg-wal`، روی ۲۴۹ اسلات را بیندازید
(`SELECT pg_drop_replication_slot('zareian_247')`)، روی ۲۴۷ بسازید
(`pg_receivewal -h 127.0.0.1 -p 15440 -U zareian_replicator --create-slot --slot=zareian_247`)،
`systemctl start zareian-pg-wal` و `systemctl start zareian-pg-basebackup` (نسخه‌ی کاملِ تازه؛ WALِ پیش از آن
بریده است).
### بازیابیِ واقعی
- **دیتابیسِ ۲۴۹ خراب شد یا داده‌ای اشتباهی پاک شد:** لحظه‌ی درست را پیدا کنید، روی ۲۴۷ با `restore-check.sh`
تا آن لحظه آزمایش کنید؛ وقتی درست بود، با `KEEP=1` اجرا کنید تا Postgresِ موقت بماند، از آن با
`pg_dump -Fc -h <پوشه> -U postgres zareian_mathapp` خروجی بگیرید و روی ۲۴۹ با `pg_restore --clean --no-owner -d zareian_mathapp` برگردد (اپ را پیش از آن stop کنید و از
وضعیتِ فعلی هم یک `pg_dump` بگیرید). این مسیر روی سرور آزموده نشده؛ با کمک بروید.
- **کلِ ۲۴۹ رفت:** همان بازیابی تا انتهای WAL روی ماشینِ تازه، و سفارش‌های چند ثانیه‌ی آخر را با پنلِ زرین‌پال/بازار
تطبیق دهید.
بکاپِ دستیِ SQLite (توسعه‌ی محلی):