---
title: "גיבוי, שחזור לנקודת זמן ותרגול שחזור"
description: "גבו את Quire, שחזרו אותו לנקודת זמן והוכיחו זאת באמצעות תרגול השחזור."
image: "https://docs.quirelms.com/og.png"
---

> Documentation Index
> Fetch the complete documentation index at: https://docs.quirelms.com/he/llms.txt
> Use this file to discover all available pages before exploring further.

# גיבוי, שחזור לנקודת זמן ותרגול שחזור

<span id="backup-point-in-time-recovery-and-the-restore-drill"></span>

התכנון נמצא בסעיף 8 של `docs/architecture/23-ops.md`. זו runbook למוצר Docker
Compose. היא נכתבה כדי שמישהו שלא כתב אותה יוכל לפעול לפיה; אם שלב כלשהו אינו ברור,
זה פגם במסמך.

## מה מוגן ואיך <!--quire:what-is-protected-and-how-->

| נכס | אופן ההגנה | היכן |
| --- | --- | --- |
| מסד הנתונים | WAL נשמר ברציפות, לכל היותר כל 60 שניות, מההפעלה הראשונה | `pgwal` volume |
| מסד הנתונים | גיבויי בסיס באמצעות `pg_basebackup`, מדי יום כברירת מחדל (`backup-scheduler`) | `pgbackup` volume |
| מסד הנתונים | עותקים מוצפנים של גיבויי הבסיס וה-WAL כל חמש דקות (`backup-offsite`) | מאגר נפרד שתציינו |
| קבצים | ה-volume של `files`. העתיקו אותו בכלי הגיבוי של המארח או השתמשו באחסון אובייקטים עם גרסאות | `files` volume |
| סודות | `docker/.env`, ובעיקר `QUIRE_MASTER_KEY` (וכן `QUIRE_MASTER_KEY_RETIRED` אם עדיין משתמשים בו), `QUIRE_BACKUP_ENCRYPTION_KEY` ו-`docker/secrets/audit-signing-key.pem` | שמרו עותק מחוץ למארח הזה |
| אינדקסי חיפוש, מטמונים והמרות | אינם מגובים; נבנים מחדש | |

יעדים: נקודת שחזור שמרוחקת לכל היותר 60 שניות מהכשל, ושחזור בתוך 60 דקות למסד
נתונים של 500 GB.

שתי טעויות נפוצות. מסד נתונים ששוחזר **ללא הקבצים שלו** מציג עמודים שבורים. מסד
ששוחזר **ללא `QUIRE_MASTER_KEY`** אינו יכול לפענח את פרטי הגישה של SSO,‏ webhook
ואינטגרציות שבו; עד לסיום החלפת המפתח הראשי בלי פריטים שלא נפתרו
([key-rotation.md](/he/ops/key-rotation/)), נכללים בכך גם המפתחות שפרשו. שניהם חלק מהגיבוי.

## יצירת גיבויים <!--quire:taking-backups-->

גיבוי בסיס של כל האשכול:

```sh
docker compose -f docker/compose.yaml --profile backup run --rm backup
```

הוא שומר את גיבויי הבסיס החדשים ביותר לפי `QUIRE_BACKUP_KEEP` (ברירת מחדל 5)
ומוחק מקטעי WAL שהגיבוי הישן ביותר כבר אינו צריך, כדי שהארכיון לא יגדל ללא גבול.
תזמנו אותו מדי יום באמצעות cron או systemd timer במארח:

```cron
15 2 * * * cd /srv/quire && docker compose -f docker/compose.yaml --profile backup run --rm backup >> /var/log/quire-backup.log 2>&1
```

או תנו ל-stack לתזמן אותו: פרופיל `backup` מריץ את `backup-scheduler`, שיוצר גיבוי
בסיס כל `QUIRE_BACKUP_INTERVAL_HOURS` שעות (ברירת מחדל 24), ואת `backup-offsite`
שמתואר בהמשך.

```sh
docker compose -f docker/compose.yaml --profile backup up -d
```

## עותקים מוצפנים מחוץ למארח <!--quire:encrypted-off-host-copies-->

שני ה-volumes נמצאים באותו מארח כמו מסד הנתונים, וגיבוי במחשב שנכשל אינו גיבוי.
`backup-offsite` מעתיק כל גיבוי בסיס וכל מקטע WAL שנשמר למאגר נפרד דרך ממשק האחסון,
מצפין אותם ושומר לפי מדיניות השמירה:

- **הצפנה.** AES-256-GCM באמצעות `QUIRE_BACKUP_ENCRYPTION_KEY` (או הקובץ שמציינת
  `QUIRE_BACKUP_ENCRYPTION_KEY_FILE`): 32 bytes שנוצרו באמצעות `openssl rand -hex 32`.
  לכל קובץ nonce ותג אימות משלו, כך שאי אפשר לקרוא עותק ללא המפתח וכל שינוי בו
  מזוהה. שמרו את המפתח יחד עם `QUIRE_MASTER_KEY`, רחוק מהמארח וממאגר הגיבויים.
  בלי המפתח אין אפשרות לשחזר.
- **מיקום.** `QUIRE_BACKUP_STORAGE_DRIVER` הוא `s3`,‏ `azure` או `local` (דיסק מרוחק
  שמחובר ב-`QUIRE_BACKUP_STORAGE_ROOT`). אלה הגדרות אחסון הקבצים עם הקידומת
  `QUIRE_BACKUP_`:‏ `QUIRE_BACKUP_S3_ENDPOINT`,‏ `QUIRE_BACKUP_S3_BUCKET`,‏
  `QUIRE_BACKUP_S3_ACCESS_KEY_ID` וכן הלאה. השתמשו ב-bucket אחר מזה של הקבצים,
  ועדיף גם בחשבון אחר, עם פרטי גישה שיכולים לכתוב אך לא למחוק אם הספק מאפשר זאת.
- **שמירה.** גיבויי הבסיס החדשים ביותר לפי `QUIRE_BACKUP_OFFSITE_KEEP` (ברירת מחדל
  `QUIRE_BACKUP_KEEP`, ואם אינו מוגדר אז 7), יחד עם ה-WAL הדרוש לישן ביותר. קבוצות
  ומקטעים ישנים יותר נמחקים מהמאגר.
- **מועד.** בכל `QUIRE_BACKUP_SHIP_INTERVAL_SECONDS` שניות (ברירת מחדל 300).
  ההעברה idempotent: פריטים שכבר נשמרו מדולגים, וגיבוי בסיס נחשב שמור רק לאחר
  כתיבת ה-manifest שלו, אחרון.

אותה פקודה פועלת גם בהרצה ידנית:

```sh
docker compose -f docker/compose.yaml run --rm backup-offsite bun apps/worker/src/backups/main.ts ship
docker compose -f docker/compose.yaml run --rm backup-offsite bun apps/worker/src/backups/main.ts verify
```

כדי לשחזר במארח חדש, החזירו תחילה קבוצת גיבוי ואז בצעו את השלבים שלהלן, תוך שימוש
בתיקייה שהורדתם במקום ה-volume `pgbackup` וב-`wal-archive` שהורדתם במקום `pgwal`:

```sh
bun apps/worker/src/backups/main.ts fetch base-20260924T021500Z /srv/restore
```

## שחזור לנקודת זמן <!--quire:restoring-to-a-point-in-time-->

השתמשו בכך לאחר אובדן נתונים: ייבוא שגוי, קורס שנמחק או מיגרציית contract שיש
לבטל. הפעולה מחליפה את מסד הנתונים הפעיל, לכן תרגלו אותה קודם באמצעות התרגול שלהלן.

1. **בחרו את זמן היעד**, ב-UTC, רגע לפני הנזק:
   `2026-09-24 09:30:00+00`. יומן הביקורת (`/admin/audit`) מציג לרוב את הרגע.
2. **עצרו את כל מי שכותב**:
   `docker compose -f docker/compose.yaml stop web content worker scheduler collab`
3. **שמרו את האשכול שנפגע** עד לאימות השחזור:
   ```sh
   docker compose -f docker/compose.yaml stop postgres
   docker run --rm -v quire_postgres18-data:/from -v quire_postgres-damaged:/to alpine cp -a /from/. /to/
   ```
4. **חלצו ל-volume הנתונים את גיבוי הבסיס החדש ביותר שקדם ליעד**, ובקשו שחזור ממוקד:
   ```sh
   docker run --rm -v quire_pgbackup:/backups:ro -v quire_postgres18-data:/var/lib/postgresql postgres:18-alpine sh -euc '
     base="$(ls -1d /backups/base-* | sort | tail -n 1)"   # or the one before the target
     rm -rf /var/lib/postgresql/18/docker && mkdir -p /var/lib/postgresql/18/docker
     tar -xzf "$base/base.tar.gz" -C /var/lib/postgresql/18/docker
     touch /var/lib/postgresql/18/docker/recovery.signal
     chown -R postgres:postgres /var/lib/postgresql/18/docker && chmod 700 /var/lib/postgresql/18/docker'
   ```
5. **בצעו שחזור**: הפעילו את Postgres פעם אחת עם הגדרות השחזור מתוך override של
   Compose, כדי שהקובץ הרגיל יישאר ללא שינוי:
   ```yaml
   # docker/compose.recover.yaml
   services:
     postgres:
       command: [postgres, -c, "restore_command=cp /var/lib/postgresql/wal-archive/%f %p",
                 -c, "recovery_target_time=2026-09-24 09:30:00+00",
                 -c, recovery_target_action=promote, -c, archive_mode=off,
                 -c, max_connections=200, -c, hba_file=/etc/postgresql/pg_hba.conf]
   ```
   ה-override מחליף את כל הפקודה, לכן הוא חוזר על שתי ההגדרות הדרושות לשחזור:
   `max_connections` לא נמוך מזה של הראשי (אחרת השחזור נעצר עם
   "insufficient parameter settings") ו-`pg_hba.conf` שמחובר כ-volume.
   ```sh
   docker compose -f docker/compose.yaml -f docker/compose.recover.yaml up -d postgres
   docker compose -f docker/compose.yaml logs -f postgres   # wait for "database system is ready"
   ```
6. **בדקו לפני שמתירים כניסה**: את שרשרת הביקורת
   (`docker compose -f docker/compose.yaml run --rm worker bun tooling/audit-verify/run.ts`)
   ושהנתונים שאבדו חזרו.
7. **חזרו למצב רגיל**: `docker compose -f docker/compose.yaml up -d`. כך Postgres
   מופעל מחדש כשהארכוב פעיל ומתחיל ציר זמן WAL חדש. צרו מיד גיבוי בסיס חדש.

שם הפרויקט `quire` מופיע כקידומת של כל volume;‏ `docker volume ls` מציג את השמות
המדויקים.

## תרגול האימות המתוזמן <!--quire:the-scheduled-verification-drill-->

`backup-offsite` מריץ תרגול גם בכל `QUIRE_BACKUP_DRILL_INTERVAL_HOURS` שעות (ברירת
מחדל 168, שבועית) ושוב בהרצה הבאה אחרי כישלון. הוא מוריד את גיבוי הבסיס האחרון
מחוץ למארח ואת כל מקטעי ה-WAL שאחריו, מפענח כל אחד מהם (וכך מוכיח שהמפתח עדיין יכול
לפתוח אותם ושלא שונו), משווה כל קובץ ל-manifest שלו, בודק שהארכיון הוא ספריית נתונים
של Postgres ומוודא שאין פער ב-WAL מגיבוי הבסיס ואילך. הדוח נכתב במאגר
בתור `reports/drill-<time>.json` וגם ביומן השירות; תרגול שנכשל מציין את הקובץ או
את המקטע הראשון שחסר.

## תרגול השחזור <!--quire:the-restore-drill-->

גיבוי שמעולם לא שוחזר אינו גיבוי. התרגול משחזר ל-Postgres זמני שאין לו דבר במשותף
עם הפעיל את גיבוי הבסיס המלא החדש ביותר שקדם ליעד, יחד עם ארכיון ה-WAL, ומוכיח
שהתוצאה תקינה:

```sh
docker/scripts/restore-drill.sh                                  # to ninety minutes ago
docker/scripts/restore-drill.sh --target "2026-09-24 09:30:00+00"
```

היעד חייב להיות UTC ובדיוק בצורה הזאת. דרושים גיבוי בסיס ישן ממנו ו-WAL שמור מאוחר
ממנו: בהתקנה חדשה, צרו גיבוי בסיס והמתינו למקטע הארכוב הבא (לכל היותר דקה אם יש
כתיבות) לפני בחירת יעד שמאוחר מהגיבוי. התרגול דורש רק Docker ו-bash במארח.

כל שלב יכול להכשיל את התרגול:

1. **יכולת שחזור**: האשכול הזמני מפעיל מחדש את הרשומות עד היעד ונפתח.
2. **שלמות**: מספר השורות בכל טבלה לעומת מסד הנתונים הפעיל (`tooling/restore-drill`).
   הפעיל התקדם מאז היעד, ולכן מותר לכל טבלה הבדל בשני הכיוונים עד הגדול מבין 500
   שורות ועשירית מגודלה (כתיבות גורמות לפיגור, ומחיקות גורמות לכך שבעותק המשוחזר
   יהיו יותר שורות); partition שנוצר אחרי היעד אינו טבלה שאבדה. בהתקנה עמוסה יותר
   הרחיבו את המותר באמצעות `QUIRE_DRILL_MAX_BEHIND` ו-`QUIRE_DRILL_MAX_DRIFT_RATIO`.
   טבלה חסרה או ריקה גורמת לכישלון.
3. **תקינות**: שרשרת הגיבוב של הביקורת עוברת אימות בעותק המשוחזר.
4. **שמישות**: תפקיד היישום קורא באמצעות אבטחה ברמת שורה.
5. **זמן**: הזמן מתחילת התרגול ועד הצלחה נבדק מול `QUIRE_DRILL_RTO_SECONDS`
   (ברירת מחדל 3600).

התרגול אינו כותב לעולם למסד הנתונים הפעיל או ל-volumes שלו: ה-volumes של הגיבוי
וה-WAL מחוברים לקריאה בלבד, והאשכול הזמני מוסר בסוף, בהצלחה או בכישלון.

הגדירו את `QUIRE_DRILL_REPORT` לנתיב כדי לכתוב דוח JSON בהצלחה או בכישלון, ותזמנו
אותו ממארח Docker:

```cron
30 3 1 * * cd /srv/quire && QUIRE_DRILL_REPORT=/var/log/quire-drill.json docker/scripts/restore-drill.sh >> /var/log/quire-drill.log 2>&1
```

הריצו אותו מדי חודש ולפני כל שדרוג. תרגול שנכשל עוצר את השדרוג. פעם ברבעון בקשו
ממישהו שלא כתב את ה-runbook לבצע שחזור אמיתי לנקודת זמן במארח נוסף, תוך שימוש
במסמך הזה בלבד.

## קבצים <!--quire:files-->

קבצים מקומיים נמצאים ב-volume `files`. גבו אותו יחד עם מסד הנתונים ובאותו זמן, ושחזרו
את שניהם ביחד:

```sh
docker run --rm -v quire_files:/files:ro -v "$PWD":/out alpine tar -czf /out/files-$(date -u +%Y%m%d).tar.gz -C /files .
```

באחסון אובייקטים הפעילו גרסאות bucket ושמרו גרסאות קודמות למשך 35 יום; במקרה כזה
שחזור קבצים לנקודת זמן מתבצע באמצעות ה-bucket עצמו.

Source: https://docs.quirelms.com/he/ops/backup-restore/index.mdx
