يوضح التصميم docs/architecture/23-ops.md القسم 8. وهذا دليل تشغيل منتج Docker Compose. كُتب ليتبعه شخص لم يكتبه؛ فإذا لم تكن خطوة واضحة، فهذا عيب في الوثيقة.
ما الذي نحميه وكيف
الأصل
الطريقة
المكان
قاعدة البيانات
أرشفة WAL باستمرار، بفواصل لا تتجاوز 60 ثانية، منذ أول تشغيل
وحدة pgwal
قاعدة البيانات
نسخ أساسية باستخدام pg_basebackup يوميًا افتراضيًا (backup-scheduler)
وحدة pgbackup
قاعدة البيانات
نسخ مشفرة من النسخ الأساسية وWAL كل خمس دقائق (backup-offsite)
مخزن منفصل تحدده
الملفات
وحدة files. انسخها بأداة النسخ لدى المضيف أو استخدم تخزينًا كائنيًا بإصدارات
وحدة files
الأسرار
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 وخطافات الويب وعمليات التكامل المحفوظة فيها؛ وإلى أن يكتمل تدوير المفتاح الرئيسي دون عناصر غير محسومة (key-rotation.md) يشمل ذلك المفاتيح المتقاعدة. كلاهما جزء من النسخة الاحتياطية.
إنشاء النسخ الاحتياطية
لإنشاء نسخة أساسية من المجموعة كاملة:
docker compose -f docker/compose.yaml --profile backup run --rm backup
يحتفظ الأمر بأحدث نسخ QUIRE_BACKUP_KEEP الأساسية (الافتراضي 5)، ويحذف ملفات WAL التي لم تعد أقدم نسخة بحاجة إليها كي لا ينمو الأرشيف بلا حدود. شغّله يوميًا بواسطة cron أو مؤقت systemd على المضيف:
15 2 * * * cd /srv/quire && docker compose -f docker/compose.yaml --profile backup run --rm backup >> /var/log/quire-backup.log 2>&1
أو دع الحزمة تجدوله: يشغل ملف backup خدمة backup-scheduler التي تنشئ نسخة أساسية كل QUIRE_BACKUP_INTERVAL_HOURS (افتراضيًا 24)، كما يشغل backup-offsite الموضح أدناه.
docker compose -f docker/compose.yaml --profile backup up -d
نسخ مشفرة خارج المضيف
توجد الوحدتان على المضيف نفسه الذي توجد عليه قاعدة البيانات، والنسخة الموجودة على الجهاز المتعطل ليست نسخة احتياطية. تنسخ خدمة backup-offsite كل نسخة أساسية وكل مقطع WAL مؤرشف عبر منفذ التخزين إلى مخزن منفصل، مع تشفيرها والاحتفاظ بها وفق سياسة:
التشفير. تستخدم AES-256-GCM مع QUIRE_BACKUP_ENCRYPTION_KEY (أو الملف المحدد في QUIRE_BACKUP_ENCRYPTION_KEY_FILE): مفتاح من 32 بايت أنشئه بواسطة 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 وغيرها. استخدم حاوية مختلفة، ويفضل حسابًا مختلفًا أيضًا عن الملفات، مع بيانات اعتماد تسمح بالكتابة دون الحذف إذا أتاح الموفر ذلك.
الاحتفاظ. يحتفظ بأحدث QUIRE_BACKUP_OFFSITE_KEEP نسخ أساسية (الافتراضي QUIRE_BACKUP_KEEP إن كان محددًا، وإلا 7) وملفات WAL التي تحتاجها أقدمها؛ وتحذف النسخ والمقاطع الأقدم من المخزن.
الموعد. كل QUIRE_BACKUP_SHIP_INTERVAL_SECONDS (افتراضيًا 300). الشحن قابل للتكرار دون أثر إضافي: يتخطى ما هو مخزن، ولا تُعد النسخة الأساسية مخزنة إلا بعد كتابة بيانها في النهاية.
يمكن تنفيذ الأوامر نفسها يدويًا:
docker compose -f docker/compose.yaml run --rm backup-offsite bun apps/worker/src/backups/main.ts shipdocker compose -f docker/compose.yaml run --rm backup-offsite bun apps/worker/src/backups/main.ts verify
لاستعادة البيانات على مضيف جديد، أعد المجموعة أولًا، ثم اتبع الخطوات أدناه مع استخدام المجلد المسترجع بدل وحدة pgbackup وأرشيف wal-archive المسترجع بدل pgwal:
bun apps/worker/src/backups/main.ts fetch base-20260924T021500Z /srv/restore
الاستعادة إلى نقطة زمنية
استخدم هذا الإجراء بعد فقدان بيانات، مثل استيراد خاطئ أو حذف مقرر أو ترحيل تعاقدي تحتاج إلى التراجع عنه. فهو يستبدل قاعدة البيانات الحية؛ لذلك تدرب عليه أولًا وفق التمرين أدناه.
اختر الوقت المستهدف بالتوقيت العالمي UTC، قبل وقوع الضرر مباشرة: 2026-09-24 09:30:00+00. يوضح سجل التدقيق (/admin/audit) الوقت عادة.
أوقف كل ما يكتب إلى قاعدة البيانات: docker compose -f docker/compose.yaml stop web content worker scheduler collab
احتفظ بالمجموعة المتضررة إلى أن تتحقق من الاستعادة:
docker compose -f docker/compose.yaml stop postgresdocker run --rm -v quire_postgres18-data:/from -v quire_postgres-damaged:/to alpine cp -a /from/. /to/
فك أحدث نسخة أساسية أقدم من الوقت المستهدف في وحدة البيانات واطلب الاستعادة المحددة:
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'
نفذ الاستعادة: شغّل Postgres مرة بإعدادات الاستعادة من ملف تجاوز Compose كي لا يتغير الملف العادي:
يستبدل ملف التجاوز الأمر كاملًا، لذلك يعيد ذكر الإعدادين اللذين تعتمد عليهما الاستعادة: يجب ألا تكون قيمة max_connections أقل من قيمة الخادم الأساسي (وإلا تتوقف الاستعادة برسالة «insufficient parameter settings»)، ويجب ذكر ملف pg_hba.conf المركب.
docker compose -f docker/compose.yaml -f docker/compose.recover.yaml up -d postgresdocker compose -f docker/compose.yaml logs -f postgres # wait for "database system is ready"
تحقق من الاستعادة قبل السماح للآخرين بالدخول: تحقق من سلسلة التدقيق (docker compose -f docker/compose.yaml run --rm worker bun tooling/audit-verify/run.ts) ومن عودة البيانات المفقودة.
عد إلى الوضع العادي: docker compose -f docker/compose.yaml up -d. يعيد ذلك تشغيل Postgres مع تفعيل الأرشفة ويبدأ خط زمني جديد لـWAL. أنشئ نسخة أساسية جديدة فورًا.
تسبق البادئة quire اسم كل وحدة؛ ويعرض docker volume ls الأسماء الدقيقة.
تمرين التحقق المجدول
تنفذ backup-offsite أيضًا تمرينًا كل QUIRE_BACKUP_DRILL_INTERVAL_HOURS (افتراضيًا 168، أي أسبوعيًا)، وتعيده في التشغيل التالي بعد الإخفاق. تجلب أحدث نسخة أساسية خارج المضيف وكل مقاطع WAL اللاحقة لها، وتفك تشفيرها (لإثبات استمرار صلاحية المفتاح وعدم تعديل الملفات)، وتقارن كل ملف ببيانه، وتتحقق من أن الأرشيف دليل بيانات Postgres ومن عدم وجود فجوات في WAL منذ النسخة الاحتياطية. يُكتب التقرير في المخزن ضمن reports/drill-<time>.json وفي سجل الخدمة؛ ويسمي التمرين الفاشل الملف أو أول مقطع مفقود.
تمرين الاستعادة
النسخة التي لم تُستعد من قبل ليست نسخة احتياطية مؤكدة. يستعيد التمرين أحدث نسخة أساسية مكتملة مأخوذة قبل الوقت المستهدف، مع أرشيف WAL، إلى قاعدة Postgres مؤقتة لا تشترك بشيء مع الحية، ثم يثبت نجاح النتيجة:
docker/scripts/restore-drill.sh # to ninety minutes agodocker/scripts/restore-drill.sh --target "2026-09-24 09:30:00+00"
يكون الوقت المستهدف بتوقيت UTC وبالصيغة نفسها تمامًا. يلزم وجود نسخة أساسية أقدم منه وملفات WAL مؤرشفة تتجاوزه. في التثبيت الجديد، أنشئ نسخة أساسية وانتظر ظهور المقطع المؤرشف التالي (خلال دقيقة على الأكثر عند وجود كتابات) قبل اختيار وقت بعدها. يحتاج التمرين إلى Docker وbash على المضيف فقط.
يفشل التمرين عند إخفاق أي خطوة:
إمكانية الاستعادة: تعيد المجموعة المؤقتة تشغيل WAL حتى الوقت المستهدف وتُفتح.
الاكتمال: قارن عدد الصفوف في كل جدول بقاعدة البيانات الحية (tooling/restore-drill). تقدمت القاعدة الحية منذ الوقت المستهدف، لذا قد يختلف الجدول بمقدار أكبر القيمتين، 500 صف أو عُشر حجمه، في أي اتجاه (تتسبب الكتابات بتأخر الاستعادة، وتتسبب عمليات الحذف بزيادة ما تحتفظ به)؛ ولا يُعد القسم المنشأ بعد الوقت المستهدف جدولًا مفقودًا. وسّع الهامش في تثبيت أكثر نشاطًا باستخدام QUIRE_DRILL_MAX_BEHIND وQUIRE_DRILL_MAX_DRIFT_RATIO. ويؤدي غياب جدول أو إفراغه إلى الفشل.
السلامة: تحقق من سلسلة تجزئة التدقيق على النسخة المستعادة.
قابلية الاستخدام: تحقق من قراءة دور التطبيق عبر أمان الصفوف.
الوقت: قس المدة من البدء إلى النجاح مقابل QUIRE_DRILL_RTO_SECONDS (الافتراضي 3600).
لا يكتب التمرين أبدًا إلى قاعدة البيانات الحية أو وحداتها: تُركب وحدتا النسخ الاحتياطي وWAL للقراءة فقط، وتُحذف المجموعة المؤقتة في النهاية سواء نجح التمرين أم فشل.
عيّن QUIRE_DRILL_REPORT إلى مسار لكتابة تقرير JSON سواء نجح التمرين أم فشل، وشغّله وفق جدول من مضيف Docker:
شغله شهريًا وقبل كل ترقية. ويمنع فشل التمرين الترقية. ومرة كل ربع سنة، اطلب من شخص لم يكتب هذا الدليل تنفيذ استعادة حقيقية إلى نقطة زمنية على مضيف احتياطي بالاعتماد على هذه الوثيقة وحدها.
الملفات
توجد الملفات المحلية في وحدة files. أنشئ نسخة احتياطية منها مع قاعدة البيانات وفي الوقت نفسه، واستعدهما معًا:
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 .
عند استخدام التخزين الكائني، فعّل إصدارات الحاوية واحتفظ بالإصدارات غير الحالية 35 يومًا؛ وعندها يتولى المخزن نفسه الاستعادة إلى نقطة زمنية للملفات.