الأدلة التقنية

النسخ الاحتياطي بقاعدة 3-2-1 واختبار الاستعادة

كيف تحدد كم بيانات تقبل أن تخسر، وتبني نسخاً لا يستطيع المخترق حذفها، وتتأكد أنك تستطيع استعادتها فعلاً.

النسخ الاحتياطي بقاعدة 3-2-1 واختبار الاستعادة

كل من يدير سيرفراً يقول إن لديه نسخة احتياطية Backup، ولكن السؤال الذي يفرق بين نسخة تنقذك ونسخة تطمئنك فقط هو: متى آخر مرة قمت باستعادة Restore هذه النسخة على جهاز آخر ورأيت بياناتك تعمل؟ فالنسخة التي لم تجرب استعادتها قد تكون فارغة أو ناقصة أو على القرص نفسه الذي تعطل، وقد تكون محذوفة لأن المخترق الذي شفر سيرفرك وجدها وحذفها قبل أن يطلب الفدية. وقد عرفنا في مصطلحات الاستضافة الذاتية معنى النسخة الاحتياطية والفرق بينها وبين RAID وال Snapshot، أما هذا المقال فهو أول مقال في مرحلة التشغيل والصيانة، ويبني عليه كل ما بعده، وسوف نناقش فيه ما يلي:

  • ما الذي يضيع البيانات فعلاً، ولماذا لا يحميك RAID ولا ال Snapshot ولا النسخة المتماثلة Replica من كل الحالات.
  • هدف نقطة الاستعادة RPO وهدف زمن الاستعادة RTO، وكيف تحددهما قبل أن تختار الأداة.
  • قاعدة 3-2-1 وكيف تطبقها على سيرفر واحد أو اثنين.
  • عدم قابلية التعديل Immutability: كيف تحمي النسخ من برامج الفدية Ransomware.
  • ما الذي تنسخه، وسياسة الاحتفاظ Retention.
  • تجربة عملية كاملة مع restic: مستودع محلي، ونسخة على سيرفر لا يقبل الحذف، ونسخة خارج الموقع على S3، واستعادة حقيقية لقاعدة بيانات، ثم إتلاف المستودع عمداً لنرى كيف نكتشف التلف ونتعافى منه، وأخيراً سكربت يختبر الاستعادة كل أسبوع.

ما الذي يضيع البيانات، وما الذي يحميك منه؟

لنفرض أن لديك سيرفراً يشغل تطبيقاً وقاعدة بيانات PostgreSQL ومجلداً للملفات المرفوعة، فالبيانات يمكن أن تضيع بخمس طرق مختلفة، ولكل طريقة حماية مختلفة:

  • تعطل القرص Disk Failure: وهنا يحميك RAID، لأن القرص الثاني يحمل البيانات نفسها.
  • الحذف الخاطئ Accidental Deletion: أمر DROP TABLE على السيرفر الخطأ، أو rm -rf في المجلد الخطأ، وهنا لا يفيدك RAID ولا النسخة المتماثلة، والسبب أنهما ينقلان الحذف إلى كل نسخة في اللحظة نفسها كما شرحنا في عناقيد قواعد البيانات.
  • برامج الفدية Ransomware: المخترق الذي يصل إلى السيرفر بصلاحية root يشفر البيانات، ويحذف ال Snapshots التي على القرص نفسه، ويبحث عن أي نسخة يستطيع الوصول إليها ليحذفها أيضاً.
  • ضياع الموقع كله: حريق في مركز البيانات، أو إيقاف حسابك عند المزود، أو خطأ من المزود نفسه، وهنا تضيع كل النسخ الموجودة في المكان نفسه أو في الحساب نفسه.
  • تحديث أو ترحيل Migration فاشل: تحديث يفسد البيانات ولا تكتشفه إلا بعد أيام، فتحتاج نسخة من قبل التحديث وليس من الليلة الماضية فقط.

والصورة التالية تبين هذه الحالات الخمس، وأي حماية تنجو في كل منها:

جدول مرسوم فيه خمس حالات لضياع البيانات: تعطل القرص والحذف الخاطئ وبرامج الفدية وضياع المزود وتحديث فاشل، وأربعة أعمدة هي RAID 1 وSnapshot وReplica ونسخة احتياطية خارج الموقع، والعمود الأخير هو الوحيد الذي عليه علامة صح في الحالات الخمس
كل حماية تغطي حالات محددة، والنسخة المنفصلة في مكان آخر هي الوحيدة التي تغطيها كلها

لاحظ أن النسخة خارج الموقع لا تنجو من برامج الفدية إلا إذا كان المخترق لا يستطيع حذفها، وهذا موضوع قسم كامل بعد قليل. وبالتالي فالنسخة الاحتياطية ليست ملفاً تنسخه إلى مكان ما، وإنما نظام له ثلاثة أسئلة: كم بيانات تقبل أن تخسر، وكم تقبل أن تتوقف، وأين تضع النسخ حتى لا يصيبها ما يصيب السيرفر.

📌
في 27 يونيو 2017 أصاب البرنامج الخبيث NotPetya شبكة شركة الشحن Maersk فمسح أجهزتها، وكما روت مجلة WIRED وجد فريق الاستعادة نسخاً احتياطية لأغلب السيرفرات عمرها بين ثلاثة وسبعة أيام، ولكنه لم يجد نسخة لسيرفرات ال Domain Controllers، والسبب أن الشركة اعتمدت على أن قرابة 150 منها تتزامن مع بعضها فيكون كل واحد نسخة للآخر، ولم تحسب حساب أن تمسح كلها في وقت واحد. والذي أنقذ الشركة سيرفر واحد في مكتب في غانا كان منفصلاً عن الشبكة بسبب انقطاع الكهرباء، فبقيت عليه النسخة الوحيدة السليمة. والدرس أن التزامن ليس نسخة احتياطية، وأن نسخة واحدة منفصلة عن الشبكة كانت الفرق بين التعافي وعدمه، فلا تجعل ذلك يعتمد على الحظ.

كم تقبل أن تخسر، وكم تقبل أن تتوقف؟ RPO وRTO

قبل أن تختار أداة أو مكاناً للنسخ، حدد رقمين، والسبب أن كل قرار بعدهما يعتمد عليهما:

  • هدف نقطة الاستعادة Recovery Point Objective، واختصاراً RPO: وهو بحسب تعريف NIST النقطة الزمنية التي يجب أن تعود إليها البيانات بعد التوقف، أي بلغة العمل: كم ساعة من البيانات تقبل أن تخسر. فإذا كنت تنسخ مرة كل ليلة في الساعة الثانية وحدثت الكارثة في الثانية ظهراً، فقد خسرت اثنتي عشرة ساعة من الطلبات.
  • هدف زمن الاستعادة Recovery Time Objective، واختصاراً RTO: وهو بحسب تعريف NIST المدة التي يمكن أن يبقى فيها النظام متوقفاً، أي كم ساعة يحتاج فريقك من لحظة الكارثة حتى تعود الخدمة.

والصورة التالية تبين الرقمين على خط زمني واحد:

خط زمني عليه آخر نسخة احتياطية سليمة في الساعة الثانية فجراً، ثم الكارثة في الثانية ظهراً، ثم عودة الخدمة في السادسة مساء، والمسافة الأولى مكتوب عليها RPO اثنتا عشرة ساعة من البيانات المفقودة، والثانية RTO أربع ساعات من التوقف
ال RPO هو ما تخسره من بيانات، وال RTO هو مدة التوقف حتى تعود الخدمة

وقد يتساءل البعض: لماذا لا أجعل الرقمين صفراً؟ والإجابة أن كل تقليل لهما له ثمن، فتقليل ال RPO من يوم إلى ساعة يعني نسخاً كل ساعة، وتقليله إلى دقائق في قاعدة البيانات يعني أرشفة سجل التغييرات WAL والاستعادة إلى نقطة زمنية PITR كما في النسخ الاحتياطي لقاعدة كبيرة، أما تقليل ال RTO فيعني سيرفراً جاهزاً وخطوات استعادة مكتوبة ومجربة، لأن أطول جزء في الاستعادة غالباً ليس نقل البيانات وإنما أن يتذكر أحدهم أين كلمة مرور المستودع وما ترتيب الخطوات. لذلك اسأل صاحب العمل سؤالين بأرقام: كم ساعة طلبات نقبل أن نفقدها؟ وكم ساعة نقبل أن يتوقف الموقع؟ ثم صمم النسخ على الإجابة.

قاعدة 3-2-1

أشهر قاعدة في النسخ الاحتياطي هي 3-2-1، وتنسبها ورقة خيارات النسخ الاحتياطي من CISA إلى كتاب المصور Peter Krogh عن إدارة الملفات الرقمية، ونصها كما في الورقة:

  • 3: احتفظ بثلاث نسخ من كل ملف مهم: الأصل ونسختين احتياطيتين.
  • 2: على نوعين مختلفين من وسائط التخزين Media، حتى لا يصيب الخطر نفسه النسختين.
  • 1: واحدة منها على الأقل خارج الموقع Offsite.

والصورة التالية تبين كيف تطبقها على سيرفر واحد:

مخطط فيه الموقع الرئيسي وبداخله السيرفر الذي يحمل البيانات الحية وقرص النسخ المحلي، وسهم النسخ الليلي بينهما، ثم سهم متقطع إلى مخزن S3 في مكان آخر مع قفل، وتحتها الأرقام 3 نسخ و2 نوعان من الوسائط و1 نسخة خارج الموقع
النسخة المحلية تعطيك استعادة سريعة، والنسخة خارج الموقع تنجو مما يصيب الموقع الأول

في التطبيق أعلاه لاحظ التالي:

  • النسخة المحلية تكون على قرص آخر أو سيرفر نسخ في المركز نفسه، وفائدتها السرعة، فاستعادة 200 جيجابايت عبر الشبكة المحلية أسرع بكثير من تنزيلها من الإنترنت، وهذا يقلل ال RTO.
  • النسخة خارج الموقع تكون عند مزود آخر أو في منطقة Region أخرى أو في مكتبك، والمهم أنها لا تتأثر بحريق أو إيقاف حساب أو خطأ في المزود الأول، ولهذا فنسخة في Bucket داخل حساب المزود نفسه أضعف من نسخة عند مزود مختلف.
  • القرص الثاني في السيرفر نفسه ليس وسيطاً مختلفاً بالمعنى المقصود، لأن السيرفر إذا سرق أو احترق أو اخترق ضاع القرصان معاً.

وأنواع التخزين التي تصلح لكل نسخة، وما هو تخزين الكائنات S3، شرحناها في أنواع التخزين Block وFile وObject، أما اختيار مكان النسخة الخارجية بالتفصيل فله موضوع مخطط له في مرحلة التشغيل والصيانة.

النسخ التي لا يستطيع المخترق حذفها: Immutability

هناك مشكلة رئيسية في أغلب إعدادات النسخ التي تراها في الدروس، وهي أن السيرفر نفسه يرسل النسخ ومعه كلمة مرور المستودع ومفاتيح ال S3 بصلاحية كاملة، وبالتالي فمن يصل إلى السيرفر يستطيع أن يقرأ النسخ ويحذفها بالأدوات نفسها. وهذا ليس احتمالاً نظرياً، فدليل مكافحة برامج الفدية من CISA يوصي بنسخ مشفرة غير متصلة Offline، ويذكر صراحة أن كثيراً من برامج الفدية تبحث عن النسخ الاحتياطية التي تصل إليها فتحذفها أو تشفرها حتى لا تستطيع الاستعادة دون دفع الفدية.

والصورة التالية تبين الفرق بين مستودع يملك السيرفر صلاحية الحذف فيه، ومستودع يقبل الإضافة فقط:

مخطط من جزأين: في اليسار سيرفر مخترق يرسل أمر forget --prune وrm -rf إلى مستودع النسخ فيحذفه، وفي اليمين سيرفر مخترق يستطيع إضافة نسخة جديدة إلى rest-server بوضع append-only ولكن طلب الحذف يرد عليه بالخطأ 403 Forbidden
إذا كانت كلمة المرور التي تكتب النسخ تستطيع حذفها، فالمخترق يستطيع حذفها أيضاً

والحل أن تكون نسخة واحدة على الأقل غير قابلة للتعديل Immutable من جهة السيرفر، وهناك أربع طرق شائعة لذلك:

  • وضع الإضافة فقط Append-only: المخزن يقبل نسخاً جديدة ويرفض الحذف والتعديل، مثل rest-server مع الخيار --append-only، أو rclone serve restic بالخيار نفسه أمام أي مخزن، وحذف النسخ القديمة يتم من جهاز آخر محمي.
  • قفل الكائنات Object Lock في S3: كل ملف يكتب ومعه تاريخ احتفاظ لا يمكن حذفه قبله حتى بمفاتيح المالك في وضع Compliance، ولكن لاحظ أن المخزن نفسه يجب أن يدعمه، فمثلاً Garage لا يدعمه بحسب صفحة التوافق، وقد ذكرنا في تخزين الكائنات على سيرفرك أي الأدوات تدعمه.
  • السحب Pull بدلاً من الدفع Push: سيرفر النسخ هو الذي يتصل بسيرفر التطبيق ويسحب البيانات، فلا توجد على سيرفر التطبيق أي صلاحية للوصول إلى النسخ.
  • نسخة منفصلة فعلياً Offline: قرص يوصل ثم يفصل، أو نسخة إلى مكان لا يصل إليه السيرفر إلا وقت النسخ، وهذه أبسط الطرق وأصعبها في الالتزام.

هل وضع الإضافة فقط يحل المشكلة كاملة؟ لا، والتفصيل في توثيق restic: المخترق لا يستطيع الحذف، ولكنه يستطيع أن يضيف نسخاً فارغة بتواريخ قريبة، فإذا شغل المسؤول لاحقاً سياسة مثل «احتفظ بآخر ثلاث نسخ أسبوعية» فقد تحذف النسخ السليمة ويبقى ما أضافه المخترق. لذلك يوصي التوثيق باستخدام --keep-within مع المستودعات التي تقبل الإضافة فقط، لأنها تحتفظ بكل النسخ داخل مدة زمنية محددة مهما كان عددها، وبتشغيل الحذف من جهاز منفصل وآمن، وبمراقبة عدد النسخ وتواريخها حتى تلاحظ أي نسخ غريبة.

ما الذي تنسخه، وما الذي لا تنسخه

النسخ الاحتياطي لا يعني أن تنسخ القرص كله، وإنما أن تنسخ ما لا تستطيع إعادة بنائه، وفيما يلي القائمة التي نبدأ بها في كل سيرفر:

  • قواعد البيانات: لا تنسخ مجلد بيانات PostgreSQL أو MySQL وهو يعمل، والسبب أن الملفات تتغير أثناء النسخ فتحصل على نسخة غير متسقة Inconsistent قد لا تعمل، وإنما خذ تفريغاً Dump بالأداة pg_dump ثم انسخ الملف الناتج، أو استخدم أداة النسخ الفيزيائي للقواعد الكبيرة كما في النسخ الاحتياطي لقاعدة كبيرة.
  • ال Volumes والملفات المرفوعة: وطريقة الوصول إليها وأرشفتها في نسخ ال Volumes.
  • ملفات الإعداد: ملفات Compose و.env وإعدادات ال Reverse Proxy والشهادات إن لم تكن تصدر تلقائياً، فهي صغيرة ولكن إعادة كتابتها من الذاكرة تأخذ ساعات.
  • الأجهزة الافتراضية كاملة: إذا كنت تشغل Proxmox فالنسخة الكاملة للجهاز بالأداة vzdump موضوع آخر شرحناه في النسخ الاحتياطي في Proxmox.

أما ما لا تنسخه فهو ما يعاد بناؤه بأمر واحد، مثل ال Docker Images التي تنزلها من السجل Registry، ومجلدات ال Cache والسجلات المؤقتة، فنسخها يزيد الحجم والوقت دون فائدة. وهناك شيء واحد يجب أن تنسخه بعيداً عن كل شيء آخر، وهو كلمة مرور المستودع ومفاتيحه، والسبب أن الأدوات الحديثة تشفر النسخ، فإذا ضاع السيرفر ومعه الملف الوحيد الذي فيه كلمة المرور، أصبحت النسخ كلها بيانات مشفرة لا يقرؤها أحد، وهذا ما تقوله لك restic نفسها عند إنشاء المستودع كما سنرى.

كم نسخة تحتفظ بها؟ سياسة الاحتفاظ Retention

لو احتفظت بنسخة واحدة فقط، فالتحديث الفاسد الذي لم تكتشفه إلا بعد ثلاثة أيام يكون قد وصل إلى نسختك الوحيدة، ولو احتفظت بكل نسخة يومية إلى الأبد لامتلأ المخزن. والحل المعتاد أن تحتفظ بنسخ كثيرة قريبة وقليلة بعيدة، كما تبين الصورة التالية:

خط زمني من اليوم إلى ستة أشهر مضت، وعليه سبعة مربعات خضراء للنسخ اليومية بالخيار keep-daily 7، وأربعة زرقاء للنسخ الأسبوعية بالخيار keep-weekly 4، وستة برتقالية للنسخ الشهرية بالخيار keep-monthly 6
سبع نسخ يومية وأربع أسبوعية وست شهرية تعطيك 17 نقطة استعادة تصل إلى نصف سنة

وقد يتساءل البعض: هل 17 نسخة تعني 17 ضعف الحجم؟ والإجابة لا، والسبب أن الأدوات الحديثة مثل restic تقسم الملفات إلى أجزاء وتحفظ كل جزء مرة واحدة فقط، وهذا ما نسميه إزالة التكرار Deduplication، فالنسخة الجديدة لا تضيف إلا ما تغير فعلاً، وسوف نرى ذلك بالأرقام في التجربة.

تجربة عملية: 3-2-1 مع restic واستعادة حقيقية

الآن سوف نقوم ببناء النظام كاملاً بالأداة restic برخصة BSD-2-Clause، وهي تشفر النسخ وتزيل التكرار وتدعم أنواعاً كثيرة من المخازن. أما لماذا restic وليس Borg أو Kopia فقد قارنا الأدوات الثلاث على البيانات نفسها في restic أم Borg أم Kopia؟، والتجربة تتبع الصورة التالية:

مخطط التجربة: سيرفر التطبيق فيه الملفات المرفوعة وتفريغ pg_dump تقرؤهما restic 0.19.1 وترسل إلى مستودع محلي على قرص ثان وإلى rest-server بوضع append-only مع قفل، ثم ينسخ المستودع المحلي بالأمر restic copy إلى S3 خارج الموقع على Garage، وساعة تشير إلى اختبار استعادة كل أسبوع
ثلاث وجهات: مستودع محلي سريع، ونسخة لا تقبل الحذف، ونسخة خارج الموقع

نشغل كل شيء داخل Docker على جهاز واحد، فال Volumes تمثل الأقراص والسيرفرات المختلفة، وهذا مناسب لفهم الخطوات فقط، أما في الإنتاج فكل وجهة على جهاز أو مزود مختلف فعلاً. وإذا لم يكن Docker مثبتاً لديك فابدأ من دليل تثبيت Docker.

تجهيز البيانات وكلمة مرور المستودع

نبدأ بقاعدة PostgreSQL فيها جدول طلبات من 200 ألف صف، و Volume للملفات المرفوعة فيه 160 صورة وخمسة ملفات CSV متطابقة عن قصد حتى نرى إزالة التكرار:

mkdir -p ~/lab-backup && cd ~/lab-backup
docker network create lab-backup-net
docker run -d --name lab-backup-db --network lab-backup-net \
  -e POSTGRES_PASSWORD=labpass -e POSTGRES_DB=shop \
  -v lab-backup-pgdata:/var/lib/postgresql postgres:18

docker exec lab-backup-db psql -U postgres -d shop \
  -c "CREATE TABLE orders (id bigserial primary key, customer text, city text, total numeric(10,2), created_at timestamptz);" \
  -c "INSERT INTO orders (customer, city, total, created_at)
      SELECT 'customer-'||g, (ARRAY['Riyadh','Jeddah','Dammam','Makkah','Madinah'])[1+g%5],
             round((random()*900+20)::numeric,2), now() - (g||' minutes')::interval
      FROM generate_series(1,200000) g;"

docker run --rm -v lab-backup-uploads:/u alpine:3.24 sh -c '
  mkdir -p /u/2026/09 /u/2026/10
  i=0; while [ $i -lt 120 ]; do head -c $(( (i*7919 % 400 + 60) * 1024 )) /dev/urandom > /u/2026/09/photo-$i.jpg; i=$((i+1)); done
  i=0; while [ $i -lt 40 ]; do head -c $(( (i*104729 % 300 + 40) * 1024 )) /dev/urandom > /u/2026/10/photo-$i.jpg; i=$((i+1)); done
  for n in 1 2 3 4 5; do seq 1 40000 | sed "s/^/invoice line /" > /u/2026/10/invoices-$n.csv; done
  du -sh /u; find /u -type f | wc -l'

والمخرج سوف يكون كما يلي:

42.0M	/u
165

بعد ذلك نولد كلمة مرور المستودع في ملف، ونعرف دالتين في ال Shell حتى لا نكرر أمر docker run الطويل، الأولى تفرغ قاعدة البيانات إلى Volume اسمه lab-backup-dumps، والثانية تشغل restic بالإعداد الثابت:

openssl rand -base64 32 > restic-pass && chmod 600 restic-pass

dump_db() {
  docker run --rm --network lab-backup-net -v lab-backup-dumps:/dumps -e PGPASSWORD=labpass \
    postgres:18 pg_dump -h lab-backup-db -U postgres -d shop -Fc -f /dumps/shop.dump
}

restic_local() {
  docker run --rm -i --network lab-backup-net --hostname app-server \
    -v lab-backup-uploads:/data/uploads:ro \
    -v lab-backup-dumps:/data/dumps:ro \
    -v lab-backup-repo:/repo \
    -v lab-backup-restore:/restore \
    -v "$PWD/restic-pass:/run/secrets/restic-pass:ro" \
    -e RESTIC_REPOSITORY=/repo -e RESTIC_PASSWORD_FILE=/run/secrets/restic-pass \
    restic/restic:0.19.1 "$@"
}

dump_db
restic_local init

والمخرج سوف يكون كما يلي:

created restic repository ee85c39e03 at /repo

Please note that knowledge of your password is required to access
the repository. Losing your password means that your data is
irrecoverably lost.

في الإعداد أعلاه لاحظ التالي:

  • الرسالة الأخيرة هي ما ذكرناه في قسم ما تنسخه: ضياع كلمة المرور يعني ضياع البيانات نهائياً، لذلك ضع نسخة من الملف restic-pass في مدير كلمات المرور الخاص بالفريق، ولا تتركها على السيرفر وحده.
  • الخيار --hostname app-server مهم مع Docker، والسبب أن restic تسجل اسم الجهاز مع كل نسخة وتبحث عن النسخة السابقة لنفس الجهاز والمسار حتى تتجاوز الملفات التي لم تتغير، و Docker يعطي كل Container اسماً عشوائياً، فدون هذا الخيار تبدو كل نسخة كأنها من جهاز جديد.
  • ربطنا البيانات للقراءة فقط :ro، فأداة النسخ لا تحتاج أن تكتب في بياناتك.
  • الصيغة -Fc في pg_dump تعني الصيغة المخصصة Custom Format، وهي مضغوطة وتستعيدها بالأداة pg_restore.
  • ثبتنا ال Image على الإصدار 0.19.1، وهو آخر إصدار عند كتابة المقال بحسب صفحة الإصدارات.

النسخة الأولى

restic_local backup --tag daily /data
restic_local snapshots

والمخرج سوف يكون كما يلي، بعد حذف شريط التقدم:

no parent snapshot found, will read all files

Files:         166 new,     0 changed,     0 unmodified
Dirs:            6 new,     0 changed,     0 unmodified
Added to the repository: 41.237 MiB (40.324 MiB stored)

processed 166 files, 44.035 MiB in 0:04
snapshot 99e744a3 saved
ID        Time                 Host        Tags        Paths  Size
------------------------------------------------------------------------
99e744a3  2026-10-07 15:18:08  app-server  daily       /data  44.035 MiB
------------------------------------------------------------------------
Timestamps shown in UTC
1 snapshots

لاحظ أن restic قرأت 44.035 ميجابايت ولكنها أضافت إلى المستودع 41.237 فقط، والفرق هو ملفات CSV الخمسة المتطابقة، فقد حفظت أجزاءها مرة واحدة، وهذه هي إزالة التكرار. أما الفرق الصغير بين Added وstored فهو الضغط Compression، والصور العشوائية لا تضغط، وهذا قريب مما يحدث مع صور JPEG حقيقية لأنها مضغوطة أصلاً.

النسخة الثانية: ما الذي يضاف فعلاً؟

لنفرض أن يوماً مر، فأضيفت ألفا طلب جديد وثلاث صور، ثم جاء وقت النسخة الليلية:

docker exec lab-backup-db psql -U postgres -d shop -qc \
  "INSERT INTO orders (customer, city, total, created_at)
   SELECT 'customer-'||g, 'Riyadh', 99.50, now() FROM generate_series(200001,202000) g;"
docker run --rm -v lab-backup-uploads:/u alpine:3.24 sh -c \
  'for i in 40 41 42; do head -c 300000 /dev/urandom > /u/2026/10/photo-$i.jpg; done'

dump_db
restic_local backup --tag daily /data
restic_local snapshots
restic_local stats --mode raw-data

والمخرج سوف يكون كما يلي:

using parent snapshot 99e744a3

Files:           3 new,     1 changed,   165 unmodified
Dirs:            0 new,     5 changed,     1 unmodified
Added to the repository: 3.188 MiB (2.981 MiB stored)

processed 169 files, 44.904 MiB in 0:02
snapshot 994d3c3e saved
ID        Time                 Host        Tags        Paths  Size
------------------------------------------------------------------------
99e744a3  2026-10-07 15:18:08  app-server  daily       /data  44.035 MiB
994d3c3e  2026-10-07 15:19:17  app-server  daily       /data  44.904 MiB
------------------------------------------------------------------------
Timestamps shown in UTC
2 snapshots
Stats in raw-data mode:
     Snapshots processed:  2
        Total Blob Count:  182
 Total Uncompressed Size:  44.430 MiB
              Total Size:  43.298 MiB
    Compression Progress:  100.00%
       Compression Ratio:  1.03x
Compression Space Saving:  2.55%

في المخرج أعلاه لاحظ التالي:

  • لدينا نسختان كاملتان يمكن استعادة أي منهما، كل واحدة بحجم 44 ميجابايت تقريباً، ومع ذلك فالمستودع كله 43.3 ميجابايت فقط.
  • الملف الذي تغير هو تفريغ قاعدة البيانات، وبما أن الصيغة المخصصة مضغوطة فتغيير صغير في البيانات يغير أغلب الملف، ولذلك أضيف قرابة 2.3 ميجابايت للتفريغ وقرابة 0.9 للصور الثلاث. وإذا كانت قاعدتك كبيرة فالتفريغ بصيغة SQL نصية غير مضغوطة يجعل إزالة التكرار أفضل بكثير، لأن الأجزاء التي لم تتغير تبقى متطابقة، والضغط تقوم به restic نفسها.

بعد ذلك نطبق سياسة الاحتفاظ التي ذكرناها، ونبدأ دائماً بالخيار --dry-run حتى نرى ما سيحذف قبل أن يحذف:

restic_local forget --keep-daily 7 --keep-weekly 4 --keep-monthly 6 --dry-run

والمخرج سوف يكون كما يلي:

Applying Policy: keep 7 daily, 4 weekly, 6 monthly snapshots
keep 2 snapshots:
ID        Time                 Host        Tags        Reasons                  Paths  Size
-------------------------------------------------------------------------------------------------
99e744a3  2026-10-07 15:18:08  app-server  daily       oldest daily snapshot    /data  44.035 MiB
                                                       oldest weekly snapshot
                                                       oldest monthly snapshot
994d3c3e  2026-10-07 15:19:17  app-server  daily       daily snapshot           /data  44.904 MiB
                                                       weekly snapshot
                                                       monthly snapshot
-------------------------------------------------------------------------------------------------
Timestamps shown in UTC
2 snapshots

لا يوجد شيء للحذف لأن النسختين في اليوم نفسه، ولكن عمود Reasons يبين لماذا تبقى كل نسخة، وهذا ما تراجعه قبل أن تزيل --dry-run وتضيف --prune الذي يحذف البيانات التي لم تعد تستخدمها أي نسخة.

النسخة خارج الموقع على S3

للنسخة الثالثة نستخدم Garage، وهو تخزين كائنات متوافق مع S3 شرحنا تشغيله بالتفصيل في تجربة Garage، لذلك نختصر هنا على الأوامر:

cat > garage.toml <<EOF
metadata_dir = "/var/lib/garage/meta"
data_dir = "/var/lib/garage/data"
db_engine = "sqlite"
replication_factor = 1
rpc_bind_addr = "[::]:3901"
rpc_public_addr = "127.0.0.1:3901"
rpc_secret = "$(openssl rand -hex 32)"

[s3_api]
s3_region = "garage"
api_bind_addr = "[::]:3900"

[admin]
api_bind_addr = "[::]:3903"
admin_token = "$(openssl rand -base64 32)"
EOF

docker run -d --name lab-backup-garage --network lab-backup-net \
  -v "$PWD/garage.toml:/etc/garage.toml:ro" -v lab-backup-garage:/var/lib/garage \
  dxflrs/garage:v2.4.1 /garage server --single-node

docker exec lab-backup-garage /garage bucket create restic-offsite
docker exec lab-backup-garage /garage key create restic-app-server
docker exec lab-backup-garage /garage bucket allow --read --write restic-offsite --key restic-app-server

والمخرج المهم من الأمر الثاني هو المفتاح، وقد أخفينا السر:

Key ID:              GKa8b2294090205bcfa612187b
Key name:            restic-app-server
Secret key:          53e41e42…
Can create buckets:  false

نضع المفتاح في ملف بيئة، ونولد كلمة مرور مختلفة للمستودع الخارجي، ثم نعرف دالة ثالثة تتصل بال S3 وتعرف المستودع المحلي مصدراً للنسخ:

cat > s3.env <<'EOF'
AWS_ACCESS_KEY_ID=GKa8b2294090205bcfa612187b
AWS_SECRET_ACCESS_KEY=ضع-السر-هنا
AWS_DEFAULT_REGION=garage
EOF
chmod 600 s3.env
openssl rand -base64 32 > restic-pass-offsite && chmod 600 restic-pass-offsite

restic_offsite() {
  docker run --rm -i --network lab-backup-net --hostname app-server \
    -v lab-backup-repo:/repo \
    -v lab-backup-restore:/restore \
    -v "$PWD/restic-pass:/run/secrets/restic-pass:ro" \
    -v "$PWD/restic-pass-offsite:/run/secrets/restic-pass-offsite:ro" \
    --env-file "$PWD/s3.env" \
    -e RESTIC_REPOSITORY=s3:http://lab-backup-garage:3900/restic-offsite \
    -e RESTIC_PASSWORD_FILE=/run/secrets/restic-pass-offsite \
    -e RESTIC_FROM_REPOSITORY=/repo -e RESTIC_FROM_PASSWORD_FILE=/run/secrets/restic-pass \
    restic/restic:0.19.1 "$@"
}

restic_offsite init --copy-chunker-params
restic_offsite copy
restic_offsite snapshots

والمخرج سوف يكون كما يلي:

created restic repository 9d8fc0d870 at s3:http://lab-backup-garage:3900/restic-offsite
 with chunker parameters copied from secondary repository
...
snapshot 994d3c3e of [/data] at 2026-10-07 15:19:17.49496636 +0000 UTC by root@app-server
  copy started, this may take a while...
[0:01] 100.00%  6 / 6 packs copied

snapshot 99e744a3 of [/data] at 2026-10-07 15:18:08.404615673 +0000 UTC by root@app-server
  copy started, this may take a while...
[0:00] 100.00%  2 / 2 packs copied

snapshot afdda40f saved, copied from source snapshot 994d3c3e
snapshot 3328bfc5 saved, copied from source snapshot 99e744a3
ID        Time                 Host        Tags        Paths  Size
------------------------------------------------------------------------
3328bfc5  2026-10-07 15:18:08  app-server  daily       /data  44.035 MiB
afdda40f  2026-10-07 15:19:17  app-server  daily       /data  44.904 MiB
------------------------------------------------------------------------
Timestamps shown in UTC
2 snapshots

في الإعداد أعلاه لاحظ التالي:

  • الخيار --copy-chunker-params يجعل المستودع الجديد يقسم البيانات بالطريقة نفسها التي يقسم بها المستودع المحلي، وبدونه لا تتطابق الأجزاء بين المستودعين فيضيع أثر إزالة التكرار عند النسخ بينهما، كما يشرح توثيق العمل مع المستودعات.
  • الأمر copy ينسخ النسخ المشفرة دون أن يعيد قراءة بياناتك الأصلية، والمستودع الخارجي له كلمة مرور مختلفة، فمن يسرق أحدهما لا يفتح الآخر.
  • المفتاح الذي أعطيناه لسيرفر التطبيق لا يستطيع إنشاء Buckets، ولكنه يستطيع الحذف من هذا ال Bucket، وهذه هي النقطة الضعيفة التي يعالجها القسم التالي.
⚠️
في محاولتنا الأولى ربطنا المستودع المحلي للقراءة فقط -v lab-backup-repo:/repo:ro لأن الأمر copy يقرأ منه فقط، فبقي الأمر يعيد المحاولة دقائق دون نهاية، وفي سجل ال Container كانت الرسالة Save(<lock/931a50d793>) returned error, retrying after 22.007143977s: open /repo/locks/…: read-only file system، والسبب أن restic تكتب ملف قفل Lock في المستودع المصدر قبل أن تقرأ منه، لذلك يحتاج المصدر صلاحية الكتابة.

نسخة لا يستطيع السيرفر حذفها

الآن نشغل rest-server بوضع الإضافة فقط، ونفعل الخيار --private-repos الذي يجعل كل مستخدم يرى مستودعه فقط، ثم ننشئ مستخدماً لسيرفر التطبيق:

docker run -d --name lab-backup-rest --network lab-backup-net \
  -v lab-backup-rest:/data -e OPTIONS="--append-only --private-repos" \
  restic/rest-server:0.14.0

openssl rand -hex 16 > rest-user-pass
docker exec lab-backup-rest create_user app-server "$(cat rest-user-pass)"
docker logs lab-backup-rest

والمخرج سوف يكون كما يلي:

Adding password for user app-server

Data directory: /data
Authentication enabled
Loaded htpasswd file /data/.htpasswd
Append only mode enabled
Private repositories enabled
Group accessible repos disabled
start server on [::]:8000

نعرف دالة رابعة بعنوان المستودع على rest-server، ثم ننشئ المستودع ونأخذ نسختين:

restic_rest() {
  docker run --rm -i --network lab-backup-net --hostname app-server \
    -v lab-backup-uploads:/data/uploads:ro \
    -v lab-backup-dumps:/data/dumps:ro \
    -v "$PWD/restic-pass:/run/secrets/restic-pass:ro" \
    -e RESTIC_REPOSITORY="rest:http://app-server:$(cat rest-user-pass)@lab-backup-rest:8000/app-server/" \
    -e RESTIC_PASSWORD_FILE=/run/secrets/restic-pass \
    restic/restic:0.19.1 "$@"
}

restic_rest init
restic_rest backup --tag daily /data
restic_rest backup --tag daily /data
restic_rest snapshots --compact

والمخرج سوف يكون كما يلي:

created restic repository 5a08e4a58e at rest:http://app-server:***@lab-backup-rest:8000/app-server/
...
snapshot e2b26793 saved
...
snapshot 6ec28414 saved
ID        Time                 Host        Tags    Size
-------------------------------------------------------------
e2b26793  2026-10-07 15:37:02  app-server  daily   44.904 MiB
6ec28414  2026-10-07 15:37:29  app-server  daily   44.904 MiB
-------------------------------------------------------------
Timestamps shown in UTC
2 snapshots

الآن لنفرض أن المخترق وصل إلى سيرفر التطبيق، فوجد كلمة مرور المستودع وكلمة مرور rest-server، وحاول أن يحذف النسخ بأداة restic نفسها:

restic_rest forget --keep-last 1 --prune
restic_rest snapshots --compact

والمخرج سوف يكون كما يلي، بعد حذف أسطر تتبع ال Stack Trace:

Applying Policy: keep 1 latest snapshots
keep 1 snapshots:
...
remove 1 snapshots:
ID        Time                 Host        Tags        Paths  Size
------------------------------------------------------------------------
e2b26793  2026-10-07 15:37:02  app-server  daily       /data  44.904 MiB
------------------------------------------------------------------------
Remove(<snapshot/e2b267939b>) failed: unexpected HTTP response (403): 403 Forbidden
unable to remove snapshot/e2b267939b5aeecbe76a6e7be8b8939b730ce2e8ecc4baa53986d1290c410639 from the repository
[0:00] 0.00%  0 / 1 files deleted
failed to remove one or more snapshots
-------------------------------------------------------------
e2b26793  2026-10-07 15:37:02  app-server  daily   44.904 MiB
6ec28414  2026-10-07 15:37:29  app-server  daily   44.904 MiB
-------------------------------------------------------------
2 snapshots

وبهذا الشكل نكون قد حصلنا على نسخة لا يستطيع السيرفر حذفها مهما كانت صلاحياته عليه، والسبب أن الرفض يحدث على سيرفر آخر وليس داخل الأداة. ولاحظ أن هذا السيرفر يحتاج بدوره أن يحذف النسخ القديمة يوماً ما، فشغل forget وprune على مسار البيانات مباشرة من جهاز النسخ نفسه أو من rest-server ثان دون --append-only لا يصل إليه إلا المسؤول، واستخدم معه --keep-within كما ذكرنا في قسم Immutability.

الاستعادة الحقيقية: الملفات ثم قاعدة البيانات

النسخة لا قيمة لها حتى تستعيدها، لذلك نستعيد آخر نسخة إلى Volume فارغ ونقارن بصمة SHA-256 لكل ملف مع الأصل:

restic_local restore latest --target /restore

docker run --rm -v lab-backup-uploads:/data/uploads:ro -v lab-backup-dumps:/data/dumps:ro \
  -v lab-backup-restore:/restore alpine:3.24 sh -c '
  cd /data && find . -type f -exec sha256sum {} + | sort -k2 > /tmp/a
  cd /restore/data && find . -type f -exec sha256sum {} + | sort -k2 > /tmp/b
  wc -l < /tmp/a; diff /tmp/a /tmp/b && echo "identical: all checksums match"'

والمخرج سوف يكون كما يلي:

restoring snapshot 994d3c3e of [/data] at 2026-10-07 15:19:17.49496636 +0000 UTC by root@app-server to /restore
Summary: Restored 175 files/dirs (44.904 MiB) in 0:00
169
identical: all checksums match

ولكن تطابق الملفات لا يثبت أن قاعدة البيانات تعمل، فقد يكون التفريغ نفسه ناقصاً منذ البداية، لذلك نشغل PostgreSQL جديداً فارغاً ونستعيد فيه التفريغ، ثم نقارن عدد الصفوف ومجموع المبالغ مع القاعدة الأصلية:

docker run -d --name lab-backup-restoredb --network lab-backup-net \
  -e POSTGRES_PASSWORD=labpass -e POSTGRES_DB=shop postgres:18

time docker run --rm --network lab-backup-net -v lab-backup-restore:/restore:ro -e PGPASSWORD=labpass \
  postgres:18 pg_restore -h lab-backup-restoredb -U postgres -d shop --no-owner /restore/data/dumps/shop.dump

docker exec lab-backup-restoredb psql -U postgres -d shop -c "SELECT count(*), max(id), sum(total) FROM orders;"
docker exec lab-backup-db        psql -U postgres -d shop -c "SELECT count(*), max(id), sum(total) FROM orders;"

والمخرج سوف يكون كما يلي:

real	0m10.501s

 count  |  max   |     sum
--------+--------+-------------
 202000 | 202000 | 94116717.19
(1 row)

 count  |  max   |     sum
--------+--------+-------------
 202000 | 202000 | 94116717.19
(1 row)

الأرقام متطابقة، وقد أخذت الاستعادة عشر ثوان لهذا الحجم الصغير، وهذا الرقم هو ما يهمك عند حساب ال RTO، فقسه على بياناتك الحقيقية ولا تخمنه، لأن الاستعادة تعيد بناء الفهارس Indexes وقد تأخذ أضعاف وقت النسخ.

ماذا لو تلف المستودع نفسه؟

الأقراص تتلف بصمت أحياناً، فيتغير جزء من ملف دون أي رسالة خطأ، وهذا ما نسميه التلف الصامت Bit Rot. لذلك نتلف المستودع المحلي عمداً بكتابة 23 بايت في وسط أحد ملفات البيانات، ثم نرى ما يكتشفه الفحص العادي وما يكتشفه الفحص الكامل:

docker run --rm -v lab-backup-repo:/repo alpine:3.24 sh -c '
  P=$(find /repo/data -type f | head -1); ls -l $P
  printf "GARBAGE-GARBAGE-GARBAGE" | dd of=$P bs=1 seek=4096 conv=notrunc 2>/dev/null'

restic_local check
restic_local check --read-data

والمخرج سوف يكون كما يلي:

-r--------    1 root     root      17160677 Oct  7 15:18 /repo/data/19/19ae85fa8214d13b432b958dac72b23b3926616a6e0dbecfd0fb89292d447e93
create exclusive lock for repository
load indexes
check all packs
check snapshots, trees and blobs
[0:00] 100.00%  2 / 2 snapshots
no errors were found
...
read all data
pack 19ae85fa8214d13b432b958dac72b23b3926616a6e0dbecfd0fb89292d447e93 contains 2 errors: [blob 421c89ee1770c675bcdf9f29c31fad5cb15cd4fefcd0f6f097b14453218816de: decrypting blob <data/421c89ee> from pack 19ae85fa8214d13b432b958dac72b23b3926616a6e0dbecfd0fb89292d447e93 failed: ciphertext verification failed unexpected pack id 606dd8671f732d7ed19561aa03c71fea1a36e352c930406f21f0bebefac6b4e6]

The repository contains damaged pack files. These damaged files must be removed to repair the repository. This can be done using the following commands. Please read the troubleshooting guide at https://restic.readthedocs.io/en/stable/077_troubleshooting.html first.

restic repair packs 19ae85fa8214d13b432b958dac72b23b3926616a6e0dbecfd0fb89292d447e93
restic repair snapshots --forget
...
Fatal: repository contains errors

في المخرج أعلاه لاحظ التالي:

  • الفحص العادي check قال إنه لا توجد أخطاء، والسبب أنه يتحقق من بنية المستودع وفهارسه ولا يقرأ البيانات نفسها، لذلك فنجاحه وحده لا يعني أن نسختك سليمة.
  • الفحص الكامل --read-data قرأ كل البيانات وفك تشفيرها، فاكتشف التلف لأن التشفير المصادق Authenticated Encryption في restic يرفض أي جزء تغير ولو ببايت واحد، كما يشرح مستند التصميم الذي يذكر أن البيانات تشفر ب AES-256 في وضع CTR مع رمز التحقق Poly1305-AES.
  • الفحص الكامل يقرأ المستودع كله، وهذا مكلف مع مخزن S3 يحاسبك على التنزيل، لذلك يوفر التوثيق الخيار --read-data-subset الذي يقرأ نسبة أو جزءاً في كل مرة، فتغطي المستودع كله خلال عدة أسابيع.

والآن ما الذي يحدث لو احتجت الاستعادة من هذا المستودع التالف؟

restic_local restore latest --target /restore
ignoring error for /data/uploads/2026/09/photo-0.jpg: decrypting blob <data/421c89ee> from pack 19ae85fa8214d13b432b958dac72b23b3926616a6e0dbecfd0fb89292d447e93 failed: ciphertext verification failed
Summary: Restored 174 / 175 files/dirs (44.846 MiB / 44.904 MiB) in 0:00
Fatal: There were 2 errors

ملف واحد ضاع، وهنا تظهر فائدة النسخة الثالثة، فنستعيد من المستودع الخارجي مع الخيار --verify الذي يقرأ الملفات بعد كتابتها ويتأكد منها:

restic_offsite restore latest --target /restore --verify
restoring snapshot afdda40f of [/data] at 2026-10-07 15:19:17.49496636 +0000 UTC by root@app-server to /restore
Summary: Restored 175 files/dirs (44.904 MiB) in 0:00
verifying files in /restore
finished verifying 169 files in /restore (took 63ms)

بقي أن نصلح المستودع المحلي، فنتبع ما اقترحته restic نفسها: repair packs يحفظ ما يمكن إنقاذه من الملف التالف ويحذفه، ثم نأخذ نسخة جديدة، فإذا كانت الملفات الأصلية ما زالت على السيرفر تعيد restic تخزين الأجزاء المفقودة. وقد كررنا التجربة على ملف بيانات آخر فكان المخرج كما يلي:

restic_local repair packs 372f3d7840603c28c8e528d0c2d59f291233120463fd3da5e12d00760e5f73e6
restic_local backup --tag daily /data
restic_local check --read-data
saving backup copies of pack files to current folder
salvaging intact data from specified pack files
failed to load blob ec57026a6e2a62671bbd694f60e663272d1042f9d2c653cf4a7c0bd56dbe2109: decrypting blob <data/ec57026a> from pack 372f3d7840603c28c8e528d0c2d59f291233120463fd3da5e12d00760e5f73e6 failed: ciphertext verification failed
rebuilding index
removing salvaged pack files

Use `restic repair snapshots --forget` to remove the corrupted data blobs from all snapshots
using parent snapshot 40800147
error: parts of /data/uploads/2026/09/photo-101.jpg not found in the repository index; storing the file again

Files:           0 new,     0 changed,   169 unmodified
Dirs:            0 new,     1 changed,     5 unmodified
Added to the repository: 279.343 KiB (279.371 KiB stored)

processed 169 files, 44.904 MiB in 0:00
snapshot 6c09ea22 saved
Warning: at least one source file could not be read
...
read all data
[0:00] 100.00%  13 / 13 packs
no errors were found

لاحظ السطر storing the file again: النسخة الجديدة أعادت الجزء المفقود من الصورة، فأصبحت النسخ القديمة التي تشير إليه سليمة أيضاً، والفحص الكامل بعدها لم يجد أخطاء. أما التحذير في آخر النسخة فسببه الملف نفسه الذي لم تجد أجزاءه في الفهرس ثم خزنته من جديد، ولا يعني أن ملفاً في المصدر لم يقرأ. وهذا الإصلاح ممكن فقط لأن الأصل ما زال موجوداً، أما لو تلف المستودع بعد ضياع السيرفر فالنسخة الخارجية هي ما ينقذك، وهذا هو سبب الرقم 3 في القاعدة.

اختبار الاستعادة تلقائياً كل أسبوع

كل ما سبق لا يفيد إذا قمت به مرة واحدة ثم نسيته، لذلك نكتب سكربتاً يفحص جزءاً من البيانات، ويستعيد آخر نسخة إلى مجلد مؤقت، ويتأكد أن تفريغ قاعدة البيانات يقرأ، وأن آخر نسخة ليست قديمة، ويخرج بخطأ إذا فشل أي شرط:

#!/usr/bin/env bash
# Weekly restore test: restore the latest snapshot to a temporary folder and prove it is usable
set -euo pipefail
export RESTIC_REPOSITORY=/repo
export RESTIC_PASSWORD_FILE=/run/secrets/restic-pass
TMP=$(mktemp -d)
trap 'rm -rf "$TMP"' EXIT

echo "1) read 10% of the packs to catch damaged data"
restic check --read-data-subset=10% --quiet

echo "2) restore the latest snapshot and verify every restored file"
restic restore latest --target "$TMP" --verify --quiet

echo "3) the database dump must be readable by pg_restore"
TABLES=$(pg_restore --list "$TMP/data/dumps/shop.dump" | grep -c "TABLE DATA")
echo "   tables with data in the dump: $TABLES"
[ "$TABLES" -ge 1 ]

echo "4) the latest snapshot must be younger than 26 hours"
LAST=$(restic snapshots latest --json | jq -r '.[0].time')
AGE=$(( ( $(date +%s) - $(date -d "${LAST%%.*}" +%s) ) / 3600 ))
echo "   latest snapshot: $LAST ($AGE hours old)"
[ "$AGE" -lt 26 ]

echo "restore test passed"

شغلناه داخل Container من Image restic نفسها بعد إضافة bash وjq وأدوات PostgreSQL، والمخرج سوف يكون كما يلي:

pg_restore (PostgreSQL) 18.6
1) read 10% of the packs to catch damaged data
2) restore the latest snapshot and verify every restored file
Summary: Restored 175 files/dirs (44.904 MiB) in 0:00
3) the database dump must be readable by pg_restore
   tables with data in the dump: 1
4) the latest snapshot must be younger than 26 hours
   latest snapshot: 2026-10-07T17:45:48.88879603Z (0 hours old)
restore test passed

في السكربت أعلاه لاحظ التالي:

  • الشرط الرابع يكتشف المشكلة الأكثر شيوعاً على الإطلاق، وهي أن النسخ الليلية توقفت منذ أسابيع بسبب كلمة مرور تغيرت أو قرص امتلأ، ولم ينتبه أحد لأن المهمة المجدولة تفشل بصمت.
  • الخيار set -euo pipefail يجعل السكربت يتوقف ويخرج بخطأ عند أول أمر يفشل، فتعتمد عليه أداة الجدولة أو المراقبة.
  • شغل السكربت من مؤقت systemd أو من cron أسبوعياً، واجعله يرسل نتيجته إلى أداة مراقبة أو تنبيه، فالأهم من أن يفشل هو أن تعرف أنه فشل. وما ترسله في السجلات وما لا ترسله شرحناه في السجلات Logging.
  • هذا الاختبار الآلي لا يغني عن تمرين استعادة كامل مرة في كل ربع سنة على سيرفر جديد، تتبع فيه الخطوات المكتوبة وتقيس الوقت، فهذا هو ال RTO الحقيقي.

التنظيف

نحذف ما أنشأناه بالاسم فقط، ولا نستخدم أوامر prune الخاصة ب Docker لأنها تحذف ما يخص مشاريع أخرى على الجهاز:

docker rm -f lab-backup-db lab-backup-restoredb lab-backup-garage lab-backup-rest
docker volume rm lab-backup-pgdata lab-backup-uploads lab-backup-dumps lab-backup-repo \
  lab-backup-restore lab-backup-garage lab-backup-rest
docker network rm lab-backup-net
rm -f restic-pass restic-pass-offsite rest-user-pass s3.env garage.toml

أخطاء شائعة وقائمة تحقق

  • نسخ لم تجرب استعادتها: وهذا أخطر الأخطاء على الإطلاق، والحل اختبار آلي أسبوعي وتمرين كامل دوري.
  • كلمة مرور المستودع على السيرفر وحده: إذا ضاع السيرفر ضاعت معه القدرة على فك النسخ.
  • نسخ مجلد قاعدة البيانات وهو يعمل: خذ تفريغاً أو استخدم أداة النسخ الخاصة بالقاعدة.
  • كل النسخ عند المزود نفسه أو في الحساب نفسه: إيقاف الحساب أو خطأ المزود يأخذها كلها.
  • صلاحية الحذف على سيرفر التطبيق: المخترق يحذف النسخ قبل أن يطلب الفدية، فاجعل نسخة واحدة على الأقل لا تقبل الحذف.
  • الاكتفاء بالفحص العادي: restic check دون --read-data أو --read-data-subset لا يكتشف التلف في البيانات كما رأينا.
  • مهام مجدولة تفشل بصمت: راقب عمر آخر نسخة، وليس فقط أن المهمة تعمل.

وقائمة التحقق قبل أن تقول إن لديك نسخاً احتياطية:

  • حددت ال RPO وال RTO بأرقام مع صاحب العمل.
  • ثلاث نسخ على وسيطين، واحدة منها خارج الموقع عند مزود أو منطقة مختلفة.
  • نسخة واحدة على الأقل لا يستطيع سيرفر التطبيق حذفها.
  • قواعد البيانات تنسخ بتفريغ أو بأداتها الخاصة، وال Volumes والإعدادات ضمن النسخ.
  • كلمة مرور المستودع ومفاتيحه محفوظة خارج السيرفر، ويعرف مكانها شخصان على الأقل.
  • سياسة احتفاظ مكتوبة، ومع المستودعات التي تقبل الإضافة فقط تستخدم --keep-within.
  • اختبار استعادة آلي كل أسبوع، وتمرين استعادة كامل مقاس بالوقت كل ربع سنة.

الخلاصة

وصلنا لنهاية الموضوع، وأهم ما فيه:

  • RAID وال Snapshot والنسخة المتماثلة يحمي كل منها من حالات محددة، والنسخة المنفصلة خارج الموقع هي الوحيدة التي تحمي من الحذف وبرامج الفدية وضياع المزود معاً.
  • ابدأ برقمين: ال RPO أي كم بيانات تقبل أن تخسر، وال RTO أي كم تقبل أن تتوقف، ثم صمم النسخ عليهما.
  • قاعدة 3-2-1: ثلاث نسخ على نوعين من الوسائط، واحدة منها خارج الموقع.
  • إذا كان السيرفر يستطيع حذف نسخه فالمخترق يستطيع ذلك أيضاً، لذلك اجعل نسخة واحدة على الأقل بوضع الإضافة فقط أو Object Lock أو منفصلة فعلياً.
  • في التجربة رفض rest-server الحذف بالرد 403 رغم أن المخترق يملك كلمتي المرور، واكتشف الفحص الكامل تلفاً لم يكتشفه الفحص العادي، وأنقذتنا النسخة الخارجية من ملف ضائع.
  • النسخة التي لم تستعدها ليست نسخة، فاختبر الاستعادة آلياً كل أسبوع، وقس زمنها على بياناتك الحقيقية.

وفي مرحلة التشغيل والصيانة مواضيع مخطط لها تكمل هذا المقال، منها مكان النسخة الخارجية، ونسخ قواعد البيانات بشكل صحيح، وخطة الطوارئ.

دليلأي أداة نسخ احتياطي Backup تناسب سيرفرك؟مقارنة عملية لأشهر أدوات النسخ الاحتياطي مفتوحة المصدر على البيانات نفسها في الوقت والحجم والذاكرة، لتعرف أيها يناسب سيرفرك.دليلمصطلحات الاستضافة الذاتية Self-hosting للمبتدئينمرجع مبسط لأهم المصطلحات التي سوف تقابلها في أدلة الاستضافة الذاتية، بأسمائها الإنجليزية كما تظهر في لوحات التحكم، مع شرح قصير لكل منها.دليلأنواع التخزين وواجهة S3: أين تضع بيانات تطبيقك؟أين تضع قاعدة البيانات وملفات المستخدمين والنسخ الاحتياطية، ولماذا يناسب كلاً منها نوع مختلف من التخزين.دليلعناقيد قواعد البيانات Database Clustering ومتى تحتاجهاماذا تعني كلمة Cluster في كل قاعدة بيانات، ومتى تحتاج نسخاً أو انتقالاً تلقائياً، ومتى يكفيك سيرفر واحد مع نسخ احتياطية جيدة.دليلنقل Containers إلى خادم جديد دون فقدان البياناتكيف تنقل ال Containers مع بياناتها إلى سيرفر جديد، دفعة واحدة أو تطبيقاً بعد تطبيق، مع خطة للتراجع إذا تعثر النقل.دليلPostgreSQL للجداول الضخمة: عشرات الملايين من الصفوفماذا تفعل عندما يصل جدول الطلبات إلى عشرات الملايين من الصفوف وتبطئ لوحة التحكم ويمتلئ القرص، وكيف تقيس قبل أن تصلح.

سجل التحديثات

  • أكتوبر 2026: كتابة المقال، وتنفيذ تجربة restic 0.19.1 مع rest-server 0.14.0 بوضع الإضافة فقط وGarage v2.4.1 وPostgreSQL 18.6 في Docker، مع استعادة حقيقية وإتلاف متعمد للمستودع وسكربت اختبار الاستعادة.
نشرة عرب رووت | ArabRoot

معرفة تستحق مكاناً في بريدك.

مقالات مختارة وأدوات مفيدة وأفكار لمشروعك القادم، في رسالة واحدة كل أسبوع.

يمكنك إلغاء الاشتراك متى شئت. الخصوصية

تم استلام طلبك. افتح بريدك واضغط رابط التأكيد لإتمام الاشتراك.