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

@@ -222,9 +222,10 @@ printf ' دیتابیس و بکاپ‌ها فقط برای %s خواندنی‌
#
# با sqlite3.Connection.backup() نه cp: کپیِ ساده وسطِ یک نوشتن فایلِ نیمه‌کاره می‌دهد (sqlite3
# به‌عنوانِ فرمان روی سرور نیست). اگر بکاپ نشد، migrate اجرا نمی‌شود: دیپلوی بی‌بکاپ را
# عمداً نمی‌پذیریم. فقط ۲ بکاپِ خودکارِ آخر (db-before-deploy-*: همین انتشار و یکی قبل‌تر) نگه داشته می‌شود تا دیسکِ
# عمداً نمی‌پذیریم. فقط ۳ بکاپِ خودکارِ آخر (db-before-deploy-*: همین انتشار و دو تای قبل) نگه داشته می‌شود تا دیسکِ
# مشترک پر نشود؛ بکاپ‌های دستی با نامِ دیگر دست نمی‌خورند.
# Postgres با pg_dump (آزموده‌نشده روی سرور تا انتقال)؛ بکاپ از بیرونِ ماشین که هنوز نیست (بند ۱۷ در چک‌لیستِ لانچ).
# Postgres با pg_dump. این بکاپ فقط برای برگشتن از migrationِ خراب است؛ کپیِ زنده‌ی بیرونِ ماشین جداست
# (ops/pgbackup/، docs/operations.md «بکاپ و بازیابی»).
step "بکاپِ دیتابیس"
python - <<'PYBACKUP' || fail "بکاپِ دیتابیس نشد؛ migrate اجرا نشد و هیچ تغییری روی داده اعمال نشد."
import datetime
@@ -252,7 +253,7 @@ if db['ENGINE'] == 'django.db.backends.postgresql':
# فقط خوانا بودنِ فایل را می‌سنجد؛ بازیابیِ واقعی روی دیتابیسِ دیگر آزموده نشده.
subprocess.run(['pg_restore', '--list', dst], check=True, stdout=subprocess.DEVNULL)
print(f' {dst} ({os.path.getsize(dst) // 2**20} MB، pg_restore --list: ok)')
old = sorted(glob.glob('.deploy-backups/db-before-deploy-*.pgdump'))[:-2]
old = sorted(glob.glob('.deploy-backups/db-before-deploy-*.pgdump'))[:-3]
for path in old:
os.remove(path)
raise SystemExit(0)
@@ -285,13 +286,30 @@ if result != 'ok':
raise SystemExit(f' بکاپ ساخته شد ولی integrity_check «{result}» داد: {dst}')
print(f' {dst} ({os.path.getsize(dst) // 2**20} MB، integrity_check: ok)')
old = sorted(glob.glob('.deploy-backups/db-before-deploy-*.sqlite3'))[:-2]
old = sorted(glob.glob('.deploy-backups/db-before-deploy-*.sqlite3'))[:-3]
for path in old:
os.remove(path)
if old:
print(f' {len(old)} بکاپِ خودکارِ قدیمی‌تر پاک شد (۲ تای آخر می‌ماند).')
print(f' {len(old)} بکاپِ خودکارِ قدیمی‌تر پاک شد (۳ تای آخر می‌ماند).')
PYBACKUP
# ─── ۳.۸ نگه‌داریِ بکاپ‌ها ─────────────────────────────────────────────────
# .deploy-backups بی‌سقف بزرگ می‌شد و دیسکِ این ماشینِ مشترک ~۸۸٪ پُر است. قاعده‌ی هر انتشار:
# db-before-deploy-* ۳ تای آخر (مرحله‌ی قبل)
# local-changes/commits-* ۳ تای آخر از هر کدام
# هر چیزِ دیگر بکاپ‌های دستیِ پیش از تغییر (env-before-*، db-before-*، unit-*…): پس از ۳۰ روز
# پاک. کپیِ .env رمز دارد و یک ماه بعد کسی به آن برنمی‌گردد.
# کپیِ واقعیِ داده این‌جا نیست: کپیِ زنده روی سرورِ دیگر است (ops/pgbackup/).
step "نگه‌داریِ بکاپ‌ها"
if [ -d .deploy-backups ]; then
for pattern in 'local-changes-*.patch' 'local-commits-*.patch'; do
ls -1t .deploy-backups/$pattern 2>/dev/null | tail -n +4 | xargs -r rm -f --
done
find .deploy-backups -maxdepth 1 -type f ! -name 'db-before-deploy-*' ! -name 'local-changes-*' \
! -name 'local-commits-*' -mtime +30 -print -delete | sed 's/^/ پاک شد: /'
printf ' .deploy-backups: %s\n' "$(du -sh .deploy-backups | cut -f1)"
fi
# ─── ۴. دیتابیس ────────────────────────────────────────────────────────────
step "اعمالِ مهاجرت‌های دیتابیس"
python manage.py migrate --noinput || fail "migrate شکست خورد؛ سرویس ری‌استارت نشد."