---
title: "คู่มือการสำรองและกู้คืนข้อมูล (Backup & Restore Guide)"
titleEn: "Backup and Restore Guide"
clauses: ["4.11.2", "ก.1.6.3"]
phase: 3
category: "ส่งมอบ"
generated: false
---

# คู่มือการสำรองและกู้คืนข้อมูล (Backup & Restore Guide)

**อ้างอิง TOR:** ข้อ 4.11.2 (Backup Guide ในชุดเอกสารถ่ายโอนระบบ) และภาคผนวก ก ข้อ 1.6.3 (คู่มือ Backup/Restore + ทดสอบ Restore อย่างน้อย 1 ครั้ง + รายงาน)
**ฉบับ:** 1.0 — วันที่ 3 ตุลาคม พ.ศ. 2569
**สคริปต์ที่อ้างถึง:** `scripts/_db-common.sh`, `scripts/backup-db.sh`, `scripts/restore-db.sh`, `scripts/restore-test.sh`
**รายงานผลทดสอบ:** `/documents/restore-test-report` (สร้างอัตโนมัติด้วย `npm run report:restore-test`)

> หัวข้อ 2–6 ใช้กับระบบสาธิต (SQLite + โฟลเดอร์ไฟล์อัปโหลด) ซึ่งทดสอบแล้ว หัวข้อ 7 เป็นแนวทางสำหรับ Production (PostgreSQL 16 + Object Storage) ซึ่ง **ยังไม่ได้ดำเนินการจริง** เพราะยังไม่ได้ติดตั้งบนโครงสร้างพื้นฐานของมหาวิทยาลัย

---

## 1. ข้อมูลที่ต้องสำรอง

| ข้อมูล | ตำแหน่ง (ระบบสาธิต) | สำรองโดย | หมายเหตุ |
|---|---|---|---|
| ฐานข้อมูลทั้งหมด (ผู้ใช้ บทสนทนา คลังความรู้ config prompt ผลประเมิน Audit Log ฯลฯ) | `prisma/dev.db` (หรือไฟล์ตาม `DATABASE_URL`/`DB_PATH`) | `backup-db.sh` | ใช้ `sqlite3 .backup` ปลอดภัยแม้แอปกำลังเขียน (โหมด WAL) |
| ไฟล์อัปโหลดต้นฉบับของฐานองค์ความรู้ | `storage/uploads/` (`STORAGE_DIR`) | `backup-db.sh` (tar) | จำเป็นต่อการ re-index |
| ไฟล์สำเนาข้อมูลส่วนบุคคล (คำขอ PDPA) | โฟลเดอร์ข้าง `STORAGE_DIR` (`storage/privacy-exports`) | **ไม่รวมในสคริปต์** | สร้างใหม่ได้จากคำขอ; สำรองเพิ่มเองถ้าต้องการ |
| คีย์เข้ารหัส | `DATA_ENCRYPTION_KEY`, `AUTH_SECRET` ใน `.env` | **ต้องเก็บแยกอย่างปลอดภัย** | หาก `DATA_ENCRYPTION_KEY` หาย ข้อมูลอีเมล/ชื่อที่เข้ารหัสในไฟล์สำรองจะถอดรหัสไม่ได้ |
| ซอร์สโค้ด/config/prompt ต้นฉบับ/seed | Git repository | ระบบควบคุมเวอร์ชัน | — |
| Log | `logs/app.log` | ระบบ log ของเครื่อง/คลัสเตอร์ | ไม่จำเป็นต่อการกู้คืน |

## 2. ความต้องการ

- `bash` (รองรับ bash 3.2 บน macOS และ bash 5 บน Debian), `sqlite3` (ถ้าไม่มีสคริปต์ออกด้วยรหัส 127 พร้อมคำแนะนำติดตั้ง), `tar`, เครื่องมือ SHA-256 (`sha256sum` / `shasum` / `openssl`)
- รันจากโฟลเดอร์ใดก็ได้ สคริปต์ย้ายไปที่รากโปรเจกต์เอง

### 2.1 การหาไฟล์ฐานข้อมูล (`_db-common.sh` → `resolve_db_path`)

ลำดับความสำคัญ:
1. `DB_PATH` (สัมพัทธ์กับรากโปรเจกต์)
2. `DATABASE_URL` ใน environment
3. `DATABASE_URL` ในไฟล์ `.env`
4. ค่าเริ่มต้น `prisma/dev.db`

`DATABASE_URL` แบบ `file:./dev.db` ตีความสัมพัทธ์กับโฟลเดอร์ `prisma/` (เหมือน Prisma) และตัด query string ออก ถ้า `DATABASE_URL` ไม่ใช่ `file:` (เช่น PostgreSQL) สคริปต์ปฏิเสธพร้อมแนะนำให้ใช้ `pg_dump`/PITR ตามหัวข้อ 7

## 3. การสำรองข้อมูล (`scripts/backup-db.sh`)

```bash
bash scripts/backup-db.sh
# ตัวอย่างระบุปลายทาง/ฐานข้อมูล
BACKUP_DIR=/mnt/backup/mbu DATABASE_URL=file:./prod.db bash scripts/backup-db.sh
```

ขั้นตอนที่สคริปต์ทำ:
1. หาไฟล์ฐานข้อมูล (หัวข้อ 2.1) — ไม่พบไฟล์ = หยุดพร้อมข้อความแนะนำ
2. ตั้งชื่อไฟล์ตามเวลา `YYYYMMDD-HHMMSS` (ถ้าซ้ำในวินาทีเดียวกันต่อท้าย `-1`, `-2`, …)
3. `sqlite3 <db> ".backup '<BACKUP_DIR>/<ts>.db'"`
4. ตรวจ `PRAGMA integrity_check` ของไฟล์สำรอง — ไม่ใช่ `ok` จะลบไฟล์สำรองและออกด้วยข้อผิดพลาด; ลบไฟล์ `-wal`/`-shm` ว่างที่ค้าง ให้เหลือไฟล์ `.db` ไฟล์เดียว
5. ถ้ามีโฟลเดอร์ไฟล์อัปโหลด สร้าง `<ts>-storage.tar` (ตั้ง `COPYFILE_DISABLE=1` กัน macOS ใส่ไฟล์ `._*`)
6. พิมพ์คำสั่งกู้คืน และ **บรรทัดสุดท้ายของ stdout เป็น path ของไฟล์ `.db`** (ให้โปรแกรมอื่นอ่านต่อได้ — หน้า `/admin/backup` ใช้ค่านี้)

| ตัวแปร | ค่าเริ่มต้น | ความหมาย |
|---|---|---|
| `DB_PATH` / `DATABASE_URL` | `prisma/dev.db` | ฐานข้อมูลต้นทาง |
| `BACKUP_DIR` | `backups` | โฟลเดอร์ปลายทาง |
| `STORAGE_DIR` | `storage/uploads` | โฟลเดอร์ไฟล์อัปโหลด |

ผลลัพธ์: `backups/<ts>.db` และ `backups/<ts>-storage.tar` (ถ้ามีไฟล์อัปโหลด)

### 3.1 สำรองผ่านหน้าเว็บ

SUPER_ADMIN ใช้หน้า `/admin/backup` → **สำรองข้อมูลตอนนี้** (API `POST /api/admin/backup` เรียก `scripts/backup-db.sh`, timeout 120 วินาที) หน้าเว็บแสดงรายการไฟล์ในโฟลเดอร์สำรองพร้อมคำสั่งกู้คืนของแต่ละไฟล์ — ไม่มีปุ่มกู้คืนในหน้าเว็บเพราะต้องหยุดแอปก่อน

### 3.2 กำหนดเวลาสำรองอัตโนมัติ (แนะนำ)

```cron
# สำรองทุกวันเวลา 02:00 และเก็บ log
0 2 * * * cd /opt/mbu-buddhist-ai && bash scripts/backup-db.sh >> logs/backup.log 2>&1
# ลบไฟล์สำรองที่เก่ากว่า 30 วัน (ปรับตามนโยบายของมหาวิทยาลัย)
30 2 * * * find /opt/mbu-buddhist-ai/backups -maxdepth 1 -type f -mtime +30 -delete
```

แนวปฏิบัติ: เก็บสำเนาไว้นอกเครื่อง (อีกเครื่อง/Object Storage) อย่างน้อย 1 ชุด, เข้ารหัสไฟล์สำรองที่ส่งออกนอกเครื่อง (ไฟล์มีข้อมูลบทสนทนา), จำกัดสิทธิ์อ่านโฟลเดอร์ `backups/`

## 4. การกู้คืนข้อมูล (`scripts/restore-db.sh`)

```bash
# 1) หยุดแอป (npm run dev / npm start / server.https.js)
# 2) กู้คืน
bash scripts/restore-db.sh backups/20261002-182126.db
# 3) เปิดแอปใหม่ แล้วตรวจที่ /api/health และ /admin/dashboard
```

ขั้นตอนที่สคริปต์ทำ:
1. ตรวจอาร์กิวเมนต์ (ไม่มี = รหัสออก 2) และหาไฟล์สำรอง (absolute, สัมพัทธ์กับโฟลเดอร์ที่เรียก หรือสัมพัทธ์กับรากโปรเจกต์)
2. ปฏิเสธถ้าไฟล์สำรองเป็นไฟล์เดียวกับฐานข้อมูลปลายทาง
3. **ตรวจ `PRAGMA integrity_check` ของไฟล์สำรองก่อนเขียนทับ** — ไม่ผ่าน = ยกเลิก ฐานข้อมูลเดิมไม่ถูกแตะต้อง
4. คัดลอกทับแบบ atomic (คัดลอกเป็นไฟล์ชั่วคราวในโฟลเดอร์เดียวกันแล้ว `mv`) และลบ `-wal`/`-shm`/`-journal` ที่ค้าง
5. ถ้ามี `<ชื่อเดียวกัน>-storage.tar` แตกไฟล์กลับเข้า `STORAGE_DIR` (เลือกข้าม/ล้างก่อนได้)
6. ตรวจ `PRAGMA integrity_check` หลังกู้คืนและรายงานจำนวนตาราง

| ตัวแปร | ค่าเริ่มต้น | ความหมาย |
|---|---|---|
| `DB_PATH` / `DATABASE_URL` | `prisma/dev.db` | ฐานข้อมูลปลายทาง |
| `STORAGE_DIR` | `storage/uploads` | โฟลเดอร์ไฟล์อัปโหลดปลายทาง |
| `RESTORE_STORAGE=0` | (กู้ไฟล์อัปโหลด) | กู้เฉพาะฐานข้อมูล |
| `RESTORE_STORAGE_CLEAN=1` | (ไม่ล้าง) | ล้างโฟลเดอร์อัปโหลดก่อนแตกไฟล์ ให้ตรงกับ ณ เวลาสำรองทุกประการ |

รหัสออก: `0` สำเร็จ, `1` ผิดพลาด/ไม่ผ่าน integrity_check, `2` ใช้คำสั่งไม่ถูกต้อง

### 4.1 หลังกู้คืน

1. เปิดแอปใหม่ — ดัชนีค้นหาในหน่วยความจำสร้างใหม่จากฐานข้อมูลที่กู้คืนเมื่อมีคำขอแรก
2. ตรวจ `GET /api/health` (จำนวน chunk ต้องไม่เป็น 0 ถ้ามีคลังความรู้)
3. ที่ `/admin/audit-log` กด **ตรวจสอบความสมบูรณ์ (hash chain)**
4. ถ้าไฟล์อัปโหลดกู้คืนไม่ครบ ใช้ **Re-index** ที่ `/admin/knowledge-base` เพื่อตรวจว่าทุกเอกสารอ่านไฟล์ต้นฉบับได้ (เอกสารที่ไฟล์หายจะล้มเหลวพร้อมข้อความ)
5. ข้อมูลที่เกิดหลังเวลาสำรองจะหายไป (RPO = ช่วงห่างของการสำรอง) — แจ้งผู้ใช้ตามแผนสื่อสาร

## 5. การทดสอบการกู้คืน (`scripts/restore-test.sh`)

```bash
npm run report:restore-test      # = bash scripts/restore-test.sh
KEEP_RESTORE_TEST_FILES=1 bash scripts/restore-test.sh   # เก็บไฟล์ทดสอบไว้ตรวจย้อนหลัง
```

**ความปลอดภัย:** สคริปต์ไม่แตะฐานข้อมูลจริงและ `storage/uploads` จริง — อ่านฐานข้อมูลจริงครั้งเดียวเพื่อทำสำเนา (`sqlite3 .backup`) ไปที่ `prisma/restore-test.db` และคัดลอกไฟล์อัปโหลดไปโฟลเดอร์ชั่วคราวใต้ `backups/.restore-test-*` แล้วลบทั้งหมดเมื่อจบ

ขั้นตอน (ทุกขั้นบันทึกผล ok/fail/skip ลงรายงาน):
1. ทำสำเนาฐานข้อมูลและไฟล์อัปโหลดเพื่อทดสอบ
2. สำรองด้วย `scripts/backup-db.sh`
3. บันทึกจำนวนแถวทุกตาราง + SHA-256 ของเนื้อหาทั้งฐานข้อมูล (`.dump`)
4. เพิ่มตาราง marker `_restore_test_marker` (ข้อมูลที่เกิดหลังการสำรอง) แล้ว **ลบไฟล์ฐานข้อมูลและไฟล์อัปโหลด** (จำลองความเสียหาย)
5. กู้คืนด้วย `scripts/restore-db.sh` (วัดเวลา = RTO ที่วัดได้)
6. ตรวจ: จำนวนแถวรายตารางตรงกับก่อนสำรอง, `PRAGMA integrity_check` = ok, ตาราง marker ต้องหายไป, SHA-256 ของ `.dump` ตรงกัน, จำนวนไฟล์อัปโหลดตรงกัน
7. เขียน `docs/deliverables/restore-test-report.md` (frontmatter ของศูนย์เอกสาร, สรุปผล, ตารางรายตาราง, สภาพแวดล้อม)

รหัสออก `0` = ผ่าน, `1` = ไม่ผ่าน (รายงานถูกเขียนทั้งสองกรณี) ตัวแปรเสริม: `RESTORE_TEST_DB`, `BACKUP_DIR`, `STORAGE_DIR`, `REPORT_PATH`, `KEEP_RESTORE_TEST_FILES`

ผลการทดสอบล่าสุด ดูที่ `/documents/restore-test-report` — แนะนำให้ทดสอบซ้ำอย่างน้อยทุกไตรมาส และทุกครั้งที่เปลี่ยน schema หรือโครงสร้างพื้นฐาน

## 6. ขั้นตอนกู้คืนเหตุฉุกเฉิน (Runbook)

| สถานการณ์ | ขั้นตอน |
|---|---|
| ไฟล์ฐานข้อมูลเสีย/ลบโดยไม่ตั้งใจ | หยุดแอป → เลือกไฟล์สำรองล่าสุดที่ผ่าน integrity → `restore-db.sh` → เปิดแอป → ตรวจตามหัวข้อ 4.1 |
| ข้อมูลผิดจากการตั้งค่า AI/prompt | ไม่ต้องกู้ฐานข้อมูล — ย้อนกลับ snapshot ที่ `/admin/ai-config` หรือเวอร์ชัน prompt ที่ `/admin/prompts` |
| ไฟล์อัปโหลดหายแต่ฐานข้อมูลปกติ | `RESTORE_STORAGE_CLEAN=1` แล้วแตกเฉพาะ tar ด้วย `tar -xf backups/<ts>-storage.tar -C storage/uploads` หรือกู้คืนพร้อม DB แล้ว Re-index |
| เครื่องเสียทั้งเครื่อง | ติดตั้งระบบใหม่ตาม `/documents/deployment-guide` (ไม่ต้อง seed) → ใส่ `.env` เดิม (คีย์เข้ารหัสเดิม) → คัดลอกไฟล์สำรองจากที่เก็บนอกเครื่อง → `restore-db.sh` |
| อัปเกรดแล้ว migration ผิดพลาด | กู้จากไฟล์สำรองก่อนอัปเกรด แล้วย้อนแอปเป็นเวอร์ชันเดิม |

## 7. แนวทางสำหรับ Production (PostgreSQL 16 + Object Storage)

สคริปต์ข้างต้นใช้กับ SQLite เท่านั้น สำหรับ Production ตาม Solution Architecture แนะนำ:

| องค์ประกอบ | วิธีสำรอง | วิธีกู้คืน |
|---|---|---|
| PostgreSQL 16 | สำรองเต็มรายวันด้วย `pg_dump -Fc` หรือ snapshot ของบริการฐานข้อมูลคลาวด์ + WAL archiving สำหรับ Point-in-Time Recovery (PITR) | `pg_restore` เข้าฐานข้อมูลใหม่ หรือ PITR ไปยังเวลาก่อนเกิดเหตุ แล้วสลับ `DATABASE_URL` |
| Object Storage (ไฟล์อัปโหลด) | เปิด versioning + replication ข้าม region/บัญชี | กู้ object version ก่อนหน้า |
| Elasticsearch | snapshot repository หรือสร้างดัชนีใหม่จากฐานข้อมูล/ไฟล์ต้นฉบับ (re-index) | restore snapshot หรือ `reindex_all` |
| Redis | ไม่จำเป็นต้องกู้ (ข้อมูลชั่วคราว: rate limit, คิว, cache) | — |
| Secret (`mbu-buddhist-ai-secrets`) | เก็บใน secret manager ของคลาวด์ / Sealed Secrets ที่สำรอง key ไว้ | สร้าง Secret ใหม่จาก secret manager |
| Helm release | `helm history` + Git ของ values | `helm rollback` / ติดตั้งใหม่จาก Git |

เป้าหมายที่เสนอ (ต้องตกลงกับมหาวิทยาลัย): RPO ≤24 ชั่วโมง (สำรองรายวัน) หรือ ≤15 นาทีเมื่อใช้ PITR, RTO ≤4 ชั่วโมง และทดสอบกู้คืนบน namespace `mbu-test` อย่างน้อยไตรมาสละครั้ง พร้อมออกรายงานในรูปแบบเดียวกับ `/documents/restore-test-report`
