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

BookStack: توثيق فريقك في كتب وفصول على سيرفرك

كيف تشغل BookStack على سيرفرك وتنظم توثيق فريقك في أرفف وكتب وفصول، مع صلاحيات ودخول موحد وبحث بالعربية.

BookStack: توثيق فريقك في كتب وفصول على سيرفرك

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

وسوف نناقش في هذا المقال ما يلي:

  • متى يناسبك BookStack، والمستويات الأربعة التي يبني عليها التوثيق.
  • التثبيت مع Docker Compose وMariaDB، ومفتاح التطبيق APP_KEY الذي أصبح إلزامياً.
  • إنشاء المحتوى من الواجهة ومن ال API.
  • العربية واتجاه النص RTL بصدق: ما يعمل، وما يحتاج إلى إصلاح، وما لا يعمل.
  • البحث بالعربية بأمثلة حقيقية.
  • الأدوار والصلاحيات على مستوى الكتاب.
  • الدخول الموحد عبر authentik بال OIDC مع ربط المجموعات بالأدوار.
  • التصدير، والنسخ الاحتياطي مع اختبار استعادة كامل، والتحديث إلى إصدار أحدث.

متى يناسبك BookStack؟

BookStack تطبيق ويب مكتوب ب PHP على إطار Laravel، ويخزن بياناته في MySQL أو MariaDB، ورخصته MIT، وآخر إصدار له وقت كتابة المقال هو v26.09.1 الصادر في 29 سبتمبر 2026، ويطور اليوم على Codeberg مع نسخة على GitHub. وأهم ما يميزه أنه يفرض على التوثيق شكلاً واحداً بأربعة مستويات، وهذا ما تبينه الصورة التالية بالمحتوى الذي أنشأناه في المختبر:

رسم يوضح رف دليل الشركة وعليه كتابان هما الموارد البشرية والتقنية، وفي كل كتاب فصل، وتحت الفصول صفحات، وصفحة قائمة الأنظمة داخل كتاب التقنية مباشرة دون فصل، مع قفل على كتاب الموارد البشرية يمنع دور Viewer من رؤيته ومن رؤية كل ما تحته
الرف يجمع الكتب، والكتاب فيه فصول وصفحات، والصلاحية على الكتاب تنتقل إلى ما تحته
  • الرف Shelf: مجموعة كتب، مثل «دليل الشركة»، والكتاب الواحد يمكن أن يكون على أكثر من رف.
  • الكتاب Book: الوحدة الأساسية، مثل كتاب لكل قسم أو لكل نظام، وهو المستوى الذي تضع عليه الصلاحيات غالباً.
  • الفصل Chapter: مجموعة صفحات داخل الكتاب، وهو اختياري.
  • الصفحة Page: المحتوى نفسه، بمحرر مرئي WYSIWYG أو بمحرر Markdown.

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

التثبيت مع Docker Compose

لا يوجد Docker Image رسمية من مشروع BookStack نفسه، وإنما يذكر توثيق التثبيت خيارين من المجتمع، أشهرهما Docker Image من LinuxServer.io، وهي التي استخدمناها. ولاحظ أن هذه ال Docker Image تحفظ كل ملفات التطبيق المتغيرة تحت المسار /config، فالمرفقات في /config/www/files والصور في /config/www/images و/config/www/uploads بحسب صفحة المشروع، وهذا ما سوف نحتاجه في النسخ الاحتياطي.

وأول خطوة هي مفتاح التطبيق APP_KEY، وهو مفتاح يشفر به BookStack الجلسات وبيانات المصادقة الثنائية MFA. ومنذ الإصدار v26.09 أصبح إلزامياً، فإذا لم تحدده يظهر خطأ بدلاً من أن يستخدم BookStack قيمة افتراضية كما كان يفعل سابقاً، بحسب صفحة التحديث. والأمر التالي يولده من ال Docker Image نفسها:

docker run --rm --entrypoint /bin/bash lscr.io/linuxserver/bookstack:v26.09.1-ls287 appkey

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

base64:UZzPr40KNWl+DXEsWcnDbymab6VjP6JHh2NIJXloplY=

ضع المفتاح وكلمات مرور قاعدة البيانات في ملف .env بجانب ملف Compose، واحفظ نسخة منه في مكان آمن، لأنك إذا فقدت المفتاح فقدت معه بيانات MFA للمستخدمين:

BOOKSTACK_TAG=v26.09.1-ls287
APP_KEY=base64:...
DB_PASSWORD=...
DB_ROOT_PASSWORD=...

وملف compose.yaml هو التالي:

services:
  db:
    image: mariadb:11.8.9
    restart: unless-stopped
    environment:
      MARIADB_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
      MARIADB_DATABASE: bookstack
      MARIADB_USER: bookstack
      MARIADB_PASSWORD: ${DB_PASSWORD}
    volumes:
      - db:/var/lib/mysql
    healthcheck:
      test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
      interval: 10s
      retries: 10

  bookstack:
    image: lscr.io/linuxserver/bookstack:${BOOKSTACK_TAG}
    restart: unless-stopped
    depends_on:
      db:
        condition: service_healthy
    environment:
      PUID: "1000"
      PGID: "1000"
      TZ: Asia/Riyadh
      APP_URL: https://wiki.example.com
      APP_KEY: ${APP_KEY}
      APP_LANG: ar
      DB_HOST: db
      DB_PORT: "3306"
      DB_USERNAME: bookstack
      DB_PASSWORD: ${DB_PASSWORD}
      DB_DATABASE: bookstack
    volumes:
      - config:/config
    ports:
      - "127.0.0.1:6875:80"

volumes:
  db:
  config:

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

  • الوسم v26.09.1-ls287 ثابت، والرقم ls287 هو رقم بناء LinuxServer، فلا تستخدم latest لأن التحديث يجب أن يكون قراراً منك بعد نسخة احتياطية.
  • شرط service_healthy يجعل BookStack ينتظر حتى تصبح MariaDB جاهزة فعلاً، وسوف نرى في قسم الاستعادة لماذا يهم هذا الترتيب.
  • APP_LANG: ar يجعل العربية لغة الواجهة الافتراضية للمستخدمين الجدد والزوار، بحسب توثيق اللغة، وكل مستخدم يستطيع أن يغيرها من إعداداته.
  • APP_URL يجب أن يكون العنوان الذي يكتبه المستخدم في المتصفح تماماً، لأن BookStack يبني منه كل الروابط، والمنفذ 127.0.0.1:6875 لا يصل إليه إلا ال Reverse Proxy على السيرفر نفسه، كما شرحنا في تطبيقك خلف Reverse Proxy.

عند التشغيل الأول بالأمر docker compose up -d ينشئ BookStack جداول قاعدة البيانات، وفي المختبر ظهرت في السجل أسطر Migration عددها 104، كل منها ينتهي ب DONE، وهذه آخرها:

bookstack-1  |   2026_04_19_141616_add_revision_view_all_permission ............ 24.24ms DONE
bookstack-1  |   2026_07_27_201402_update_users_external_auth_id_collation ..... 50.35ms DONE
bookstack-1  | [custom-init] No custom files found, skipping...
bookstack-1  | [ls.io-init] done.

وبعد ذلك يدخل المدير الأول بالبريد [email protected] وكلمة المرور password، وهي قيم معروفة لكل من قرأ التوثيق، لذلك غيرهما فوراً قبل أن تجعل الموقع متاحاً من الإنترنت. وإذا غيرت APP_URL بعد ذلك، فالروابط القديمة محفوظة داخل الصفحات في قاعدة البيانات، ويحدثها الأمر php /app/www/artisan bookstack:update-url كما سوف نرى في قسم الاستعادة.

أول رف وكتاب وصفحة

من الواجهة تنشئ الرف من قائمة «الأرفف»، والكتاب من «الكتب»، ثم الفصول والصفحات من داخل الكتاب. وإذا كان لديك توثيق كثير تريد نقله فالأفضل هو ال API، وتوثيقه الكامل موجود داخل كل نسخة على المسار /api/docs، ويمكنك أن تراه على النسخة التجريبية. وللوصول إليه تنشئ توكن Token من صفحة حسابك ثم قسم الدخول والأمان /my-account/auth، فتحصل على معرف Token ID وسر Token Secret، ويرسلان في الترويسة Authorization:

curl -s -X POST https://wiki.example.com/api/books \
  -H "Authorization: Token $TOKEN_ID:$TOKEN_SECRET" -H 'Content-Type: application/json' \
  --data-binary '{"name": "المالية", "description": "الفواتير والمشتريات"}'

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

{"id": 10, "name": "المالية", "slug": "almaly", "created_by": 1}

لاحظ الحقل slug، فهو الجزء الذي يظهر في الرابط، وقد حوله BookStack من «المالية» إلى حروف لاتينية almaly، وكذلك أصبح «دليل الشركة» dlyl-alshrk و«إضافة موظف جديد إلى شبكة WireGuard» adaf-mothf-gdyd-al-shbk-wireguard. فالروابط تبقى قصيرة وتعمل في كل مكان، ولكنها لا تقرأ بالعربية، وإذا غيرت عنوان الصفحة لاحقاً تغير الرابط معه، ففي المختبر أصبح رابط «قائمة الأنظمة الداخلية» kaym-alanthm-aldakhly، لذلك ضع في الصفحات الأخرى روابط تنشئها من المحرر وليس روابط تنسخها باليد. وبهذه الطريقة أنشأنا في المختبر بسكربت Python صغير رفاً وكتابين وفصلين وأربع صفحات، والصورة التالية تبين الرف في الواجهة العربية:

واجهة BookStack v26.09 بالعربية من اليمين إلى اليسار، وفيها رف دليل الشركة وعليه كتابا الموارد البشرية والتقنية، والقائمة الجانبية فيها إجراءات الرف وتفاصيله ونشاطاته
الواجهة كلها من اليمين إلى اليسار بمجرد اختيار العربية

العربية واتجاه النص: ما يعمل وما لا يعمل

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

صفحة إضافة موظف جديد إلى شبكة WireGuard في BookStack بالعربية، وفيها خطوات مرقمة وكتلة كود وجدول عناوين واقتباس، ويظهر فيها المسار etc/wireguard/wg0.conf والشرطة المائلة في آخره بدلاً من أوله
النص العربي والجدول سليمان، أما المسار داخل السطر فقد انتقلت شرطته المائلة إلى آخره

النص العربي والعناوين والجدول والقوائم المرقمة كلها سليمة، ولكن لاحظ السطر الثاني من الخطوات، فالمسار /etc/wireguard/wg0.conf ظهر بالشكل etc/wireguard/wg0.conf/، والسبب أن الكود داخل السطر يرث اتجاه الفقرة العربية، فتنتقل الشرطة المائلة في أوله إلى الجهة الأخرى بحسب قواعد النص ثنائي الاتجاه Bidirectional Text، وهذا خطأ خطير في دليل يتبعه المهندس حرفاً بحرف. وكذلك ظهرت كتلة الكود مصفوفة إلى اليمين ورقم السطر على اليمين. والحل أن نجبر الكود على الاتجاه من اليسار إلى اليمين، وفي BookStack مكان لذلك في الإعدادات، ثم التخصيص، ثم حقل Custom HTML Head Content، فأضفنا فيه:

<style>
.page-content code, .page-content pre, .page-content .cm-editor { direction: ltr; unicode-bidi: isolate; text-align: left; }
</style>

والصورة التالية تبين الصفحة نفسها بعد الحفظ:

الصفحة نفسها بعد إضافة CSS في إعدادات التخصيص، ويظهر المسار /etc/wireguard/wg0.conf صحيحاً، وكتلة الكود من اليسار إلى اليمين ورقم السطر على اليسار
بعد سطر CSS واحد أصبح المسار صحيحاً وأصبحت كتلة الكود من اليسار إلى اليمين

أما التصدير إلى PDF فهو نقطة الضعف الحقيقية للعربية. ف BookStack يستخدم بشكل افتراضي مكتبة dompdf، ويقول توثيق PDF إنها محدودة الإمكانيات، وإن الخط الافتراضي DejaVu Sans يغطي اللغات اللاتينية أساساً. وفي المختبر ظهرت الحروف العربية متصلة بشكل صحيح، ولكن الكلمات جاءت بترتيب معكوس من اليسار إلى اليمين، كما في الصورة التالية من ملف PDF الذي صدرناه:

ملف PDF صدره BookStack بمكتبة dompdf لصفحة WireGuard، والكلمات العربية في العناوين والفقرات مرتبة من اليسار إلى اليمين فلا تقرأ، بينما الأوامر الإنجليزية سليمة
ملف PDF من dompdf: الحروف متصلة ولكن الكلمات بترتيب معكوس

وتغيير الخط لا يحل هذه المشكلة، لأن المشكلة في ترتيب الكلمات وليس في شكل الحروف. وأمامك حلان: الأول أن تصدر الصفحة بصيغة Contained Web File، وهي ملف HTML واحد فيه الصور والتنسيق، ثم تطبعه إلى PDF من المتصفح، وقد فعلنا ذلك بمتصفح Chrome فخرجت الصفحة العربية سليمة. والثاني أن تجعل BookStack يستخدم برنامجاً آخر بالخيار EXPORT_PDF_COMMAND، ويحذر التوثيق نفسه من أن هذا الخيار يشغل برنامجاً خارجياً على HTML يكتبه المستخدمون، فيجب عزله عن الشبكة والملفات، ولم نجربه في هذا المقال.

البحث بالعربية

يدعم BookStack صيغة بحث متقدمة يشرحها توثيق البحث: الكلمات العادية، والعبارة الكاملة بين علامتي تنصيص، والوسوم Tags بين أقواس مربعة، والمرشحات Filters بين أقواس معقوفة. ولكن السؤال المهم لنا هو كيف يتعامل مع العربية، حيث تلتصق «ال» و«و» و«ل» بالكلمة. فأرسلنا مجموعة استعلامات إلى المسار /api/search على المحتوى الذي أنشأناه، وكانت النتائج كما يلي:

الاستعلامعدد النتائجالملاحظة
السيرفرات4الفصل والكتاب وصفحتان
السيرف4جزء من أول الكلمة يكفي
سيرفرات0الكلمة في النص هي «السيرفرات» و«للسيرفرات»
سيرفر0السبب نفسه
إجازة1موجودة في النص بهذا الشكل
اجازة0لا يوحد البحث بين الهمزة والألف
wireguard1البحث لا يفرق بين الحروف الكبيرة والصغيرة
"المفتاح الخاص"1العبارة الكاملة
[قسم=التقنية]2البحث بالوسم وقيمته
{type:page} [قسم=التقنية]2الصفحات فقط ذات الوسم
10.8.0.121عنوان داخل جدول

والنتيجة أن البحث يطابق أول الكلمة كما كتبت في النص، فالكلمة «السيرف» تجد «السيرفرات»، ولكن «سيرفرات» لا تجدها لأن «ال» جزء من الكلمة عنده، ولا يوجد تحليل صرفي للعربية ولا توحيد للهمزات. والصورة التالية تبين صفحة النتائج في الواجهة:

صفحة نتائج البحث في BookStack عن الكلمة السيرف، وفيها أربع نتائج هي فصل السيرفرات والشبكة وصفحة WireGuard وكتاب التقنية وصفحة النسخ الاحتياطي، مع تمييز الجزء المطابق وخيارات البحث المفصل في الجانب
البحث عن «السيرف» يجد كل ما يبدأ بها، والجزء المطابق مميز

لذلك اتفق مع فريقك على ثلاث عادات: ابحث بأول الكلمة دون آخرها، وجرب الكلمة مع «ال» ودونها، واستخدم الوسوم Tags للتصنيف المهم مثل القسم والنظام، لأن البحث بالوسم دقيق ولا يتأثر بشكل الكلمة.

الأدوار والصلاحيات

في كل نسخة جديدة أربعة أدوار جاهزة: Admin وEditor وViewer وPublic للزوار غير المسجلين، وكل دور له صلاحيات عامة على النظام، وصلاحيات افتراضية على المحتوى. ثم يمكن أن تغير هذه الصلاحيات على أي رف أو كتاب أو فصل أو صفحة، ويشرح توثيق الأدوار والصلاحيات القواعد التالية:

  • الصلاحية على الكتاب أو الفصل تنتقل إلى كل ما تحته، إلا إذا كانت لعنصر منها صلاحيات خاصة به.
  • الصلاحية على الرف لا تنتقل إلى كتبه، لأن الكتاب يمكن أن يكون على أكثر من رف، ولكن يوجد زر ينسخها إلى كل كتب الرف مرة واحدة.
  • الصلاحية الخاصة بدور معين أقوى من صلاحية «الآخرين Everyone Else»، وهذه أقوى من صلاحيات الدور العامة.
  • دور Admin يحتفظ دائماً بكل الصلاحيات ولا يظهر في شاشة صلاحيات المحتوى.

وفي المختبر أنشأنا المستخدم Khalid بدور Viewer، ثم منعنا هذا الدور من رؤية كتاب الموارد البشرية، وكلاهما عبر ال API:

curl -s -X POST $B/api/users -H "$AUTH" -H 'Content-Type: application/json' \
  --data '{"name":"Khalid","email":"[email protected]","password":"Khalid-Lab-2026!","roles":[3],"language":"ar"}'

curl -s -X PUT $B/api/content-permissions/book/1 -H "$AUTH" -H 'Content-Type: application/json' \
  --data '{"role_permissions":[{"role_id":3,"view":false,"create":false,"update":false,"delete":false}]}'

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

{"owner":{"id":1,"name":"Admin","slug":"admin"},"role_permissions":[{"role_id":3,"view":false,"create":false,"update":false,"delete":false,"role":{"id":3,"display_name":"Viewer"}}],"fallback_permissions":{"inheriting":true,"view":null,"create":null,"update":null,"delete":null}}

والرقم 3 هو رقم دور Viewer كما أعاده الطلب GET /api/roles. و"inheriting":true في آخر الرد يعني أن بقية الأدوار تبقى على صلاحياتها الافتراضية. والصورة التالية تبين الصلاحية نفسها في شاشة صلاحيات الكتاب:

شاشة أذونات كتاب الموارد البشرية في BookStack، وفيها صف لدور Viewer وكل الخانات عرض وإنشاء وتحديث وحذف غير محددة، وصف للآخرين مع خيار وراثة الإعدادات الافتراضية
دور Viewer ممنوع من كل شيء في هذا الكتاب، وبقية الأدوار ترث صلاحياتها

وعندما دخل Khalid رأى الرف وعليه كتاب التقنية وحده، وعندما فتح رابط كتاب الموارد البشرية مباشرة ظهرت له رسالة «لم يتم العثور على الكتاب» مع ملاحظة أنه قد لا يملك صلاحية رؤيته، أي أن BookStack لا يؤكد له وجود الكتاب أصلاً:

رف دليل الشركة كما يراه المستخدم Khalid بدور Viewer، وعليه كتاب التقنية فقط دون كتاب الموارد البشرية، ودون أزرار التعديل
الرف نفسه كما يراه دور Viewer: كتاب واحد ولا توجد أزرار تعديل

الدخول الموحد عبر authentik بال OIDC

إذا كان لديك authentik فلا داعي لكلمات مرور منفصلة في BookStack، والفكرة شرحناها في شرح OAuth وOpenID Connect. ويذكر توثيق OIDC في BookStack ثلاثة قيود يجب أن تعرفها قبل البدء: التوقيع RS256 فقط، وعنوان المصدر Issuer يجب أن يبدأ ب https://، وBookStack يربط المستخدم بالقيمة sub وليس بالبريد.

في authentik أنشئ Application ومعه OAuth2/OpenID Provider من نوع Confidential، كما في صفحة BookStack في توثيق authentik، واجعل عنوان العودة Redirect URI هو https://wiki.example.com/oidc/callback بالمطابقة الدقيقة Strict، واختر شهادة التوقيع لكي يوقع ب RS256. وفي المختبر أنشأنا ال Provider عبر API authentik، فظهر خطأ يستحق الذكر: رجع المتصفح إلى BookStack برسالة «An unknown error occurred»، وفي سجل authentik السطر التالي:

"event": "Invalid grant_type for provider", "grant_type": "authorization_code"

والسبب أن ال Provider الذي ينشأ عبر ال API تكون قائمة grant_types فيه فارغة، فأضفنا authorization_code وrefresh_token فعمل الدخول. وإذا أنشأته من الواجهة فتأكد من هذه القائمة في إعدادات ال Provider إذا رأيت الرسالة نفسها. أما في BookStack فنضيف المتغيرات التالية إلى خدمة bookstack في ملف Compose:

      AUTH_METHOD: oidc
      AUTH_AUTO_INITIATE: "false"
      OIDC_NAME: authentik
      OIDC_DISPLAY_NAME_CLAIMS: name
      OIDC_CLIENT_ID: bookstack
      OIDC_CLIENT_SECRET: ${OIDC_CLIENT_SECRET}
      OIDC_ISSUER: https://auth.example.com/application/o/bookstack/
      OIDC_ISSUER_DISCOVER: "true"
      OIDC_END_SESSION_ENDPOINT: "true"
      OIDC_USER_TO_GROUPS: "true"
      OIDC_GROUPS_CLAIM: groups
      OIDC_REMOVE_FROM_GROUPS: "true"

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

  • عنوان OIDC_ISSUER هو عنوان ال Provider في authentik مع الشرطة المائلة في آخره، ومع OIDC_ISSUER_DISCOVER يقرأ BookStack منه بقية العناوين والمفاتيح، ويرفض الاستمرار إذا كانت قيمة issuer في الرد مختلفة عنه حرفاً بحرف.
  • OIDC_USER_TO_GROUPS وOIDC_GROUPS_CLAIM يربطان مجموعات authentik بأدوار BookStack بالاسم، فالمستخدم في المجموعة Editor يحصل على دور Editor، وOIDC_REMOVE_FROM_GROUPS يسحب منه الأدوار التي لم تعد تقابل مجموعاته عند كل دخول.
  • لم نحتج إلى OIDC_ADDITIONAL_SCOPES، لأن authentik يضع المطالبة groups ضمن ال Scope المسمى profile.

وبعد إعادة إنشاء ال Container تصبح صفحة الدخول زراً واحداً، ثم صفحة authentik:

صفحة تسجيل الدخول في BookStack بعد تفعيل OIDC، وفيها زر واحد هو تسجيل الدخول باستخدام authentik دون حقلي البريد وكلمة المرور
مع AUTH_METHOD=oidc تختفي حقول كلمة المرور ويبقى زر authentik
صفحة دخول authentik وفيها رسالة Log in to continue to BookStack وحقل البريد الإلكتروني أو اسم المستخدم بالعربية وزر تسجيل الدخول
authentik يذكر اسم التطبيق الذي طلب الدخول

دخلت المستخدمة سارة وهي في مجموعة Editor في authentik، فأنشأ BookStack حسابها تلقائياً، وأعاد الطلب GET /api/users/4 ما يلي بعد اختيار بعض الحقول:

سارة الأحمد [email protected] external_auth_id= 695de2b5fd088fca… roles= ['Editor']

والقيمة external_auth_id هي sub القادمة من authentik، وهي التي يربط بها BookStack الحساب في كل دخول بعد ذلك. والصورة التالية تبين كتاب التقنية كما تراه سارة، ومعها أزرار إنشاء الصفحات والفصول التي يعطيها دور Editor:

كتاب التقنية في BookStack كما تراه المستخدمة سارة بعد الدخول عبر authentik، وفي القائمة الجانبية أزرار صفحة جديدة وفصل جديد وتعديل ونسخ وحذف
الدور جاء من مجموعة authentik، دون أي إعداد للمستخدم داخل BookStack
⚠️
عندما تغير AUTH_METHOD إلى oidc لا يستطيع أي مستخدم أن يدخل بكلمة المرور، ومنهم المدير الذي أنشأته في البداية، لأن حسابه ليس مربوطاً بأي حساب في authentik. لذلك قبل التغيير اجعل المسؤول عن BookStack في مجموعة اسمها Admin في authentik لكي يحصل على دور Admin عند أول دخول، أو أدخل قيمة sub الخاصة به في حقل External Authentication ID في حسابه، كما يشرح التوثيق في قسم Switching to OIDC with Existing Users. وتوكنات ال API تبقى تعمل بعد التغيير.

وفي المختبر احتجنا إلى ثلاثة أشياء لا تحتاجها على سيرفر حقيقي، لأن authentik كان على الاسم auth.localhost بشهادة من جهة توقيع صنعناها للمختبر: وثقنا بشهادة هذه الجهة داخل Container الخاص ب BookStack بسكربت في المجلد /custom-cont-init.d الذي تدعمه صور LinuxServer كما في توثيق التخصيص، ثم أضفنا Container صغيراً بالأداة socat يحول المنفذ المحلي إلى authentik، لأن الأمر curl داخل ال Container حول الاسم auth.localhost إلى 127.0.0.1 دون أن يسأل DNS، وBookStack يتصل ب authentik عبر مكتبة curl نفسها في PHP. أما على سيرفرك فيكون authentik على نطاق حقيقي بشهادة Let's Encrypt فلا تحتاج إلى أي منها.

التصدير

كل صفحة وفصل وكتاب يمكن أن يصدر من قائمة «تصدير» أو من ال API، وهذه الأحجام من المختبر لصفحة WireGuard:

الصيغةالمسار في ال APIالحجمالملاحظة
Markdown/api/pages/7/export/markdown836 بايتالمحتوى كما كتب، والعربية سليمة
نص عادي/api/pages/7/export/plaintext759 بايتللبحث أو النقل إلى نظام آخر
Contained Web File/api/pages/7/export/html21,536 بايتملف HTML واحد فيه التنسيق، ويطبع من المتصفح بشكل سليم
PDF/api/pages/7/export/pdf1,132,793 بايتdompdf، والعربية بترتيب معكوس كما رأينا
Portable ZIP/api/books/2/export/zip1,849 بايت للكتابملف data.json ومجلد files، ويستورد في BookStack آخر

وحجم PDF الكبير سببه أن dompdf يضع داخل الملف خطوط DejaVu الثلاثة التي استخدمها. أما ZIP فهو الصيغة المناسبة لنقل كتاب بين نسختين من BookStack، لأنه يحفظ الشكل والوسوم والمرفقات، ولكنه لا يغني عن النسخة الاحتياطية، لأنه لا يحمل المستخدمين ولا الصلاحيات ولا سجل المراجعات.

النسخ الاحتياطي واختبار الاستعادة

يقول توثيق النسخ الاحتياطي إن هناك نوعين من البيانات: قاعدة البيانات، والملفات، وهي ملف .env ومجلدات الصور والمرفقات والقوالب Themes. وفي Docker Image الخاصة ب LinuxServer كلها تحت /config/www. ولكن لاحظ أمراً اكتشفناه في المختبر، فالملف /config/www/.env فيه قيم مثالية مثل APP_KEY=SomeRandomString، لأن القيم الحقيقية تأتي من متغيرات البيئة في ملف Compose، لذلك يجب أن تنسخ احتياطياً ملفي compose.yaml و.env اللذين بجانبه، وإلا فقدت المفتاح. والأمران التاليان يأخذان النسخة:

docker compose exec -T db sh -c 'mariadb-dump --single-transaction -u bookstack -p"$MARIADB_PASSWORD" bookstack' \
  > backup/bookstack-2026-10-09.sql
docker run --rm -v bookstack_config:/config:ro -v "$PWD/backup:/backup" alpine:3.22 \
  tar czf /backup/bookstack-files-2026-10-09.tar.gz -C /config/www .env uploads files images themes
ls -l backup
-rw-r--r-- 1 wajdy 197609 86066 Oct  9 04:05 bookstack-2026-10-09.sql
-rw-r--r-- 1 wajdy 197609 42777 Oct  9 04:05 bookstack-files-2026-10-09.tar.gz

الخيار --single-transaction يأخذ نسخة متسقة من جداول InnoDB دون أن يقفل قاعدة البيانات، فلا يتوقف الموقع أثناء النسخ. واسم ال Volume bookstack_config هو اسم المجلد ثم اسم ال Volume، فتأكد منه بالأمر docker volume ls.

ولكي نعرف أن النسخة تعمل فعلاً استعدناها في نسخة جديدة كاملة من BookStack بمشروع Compose آخر ومنفذ آخر. وفي المحاولة الأولى شغلنا قاعدة البيانات واستوردنا النسخة مباشرة، فظهر الخطأ التالي لأن MariaDB لم تكن جاهزة بعد:

ERROR 2002 (HY000): Can't connect to local server through socket '/run/mysqld/mysqld.sock' (2)

ثم شغلنا BookStack فوجد قاعدة بيانات فارغة، فأنشأ جداول جديدة فارغة، وهذا أسوأ ما يمكن أن يحدث في استعادة حقيقية لأنه يبدو كأنه نجح. لذلك يقول التوثيق إن قاعدة البيانات تستعاد قبل تشغيل Container الخاص ب BookStack، والترتيب الصحيح هو:

docker compose up -d --wait db
docker compose exec -T db sh -c 'mariadb -u bookstack -p"$MARIADB_PASSWORD" bookstack' < backup/bookstack-2026-10-09.sql
docker run --rm -v bookstack_config:/config -v "$PWD/backup:/backup:ro" alpine:3.22 \
  sh -c 'mkdir -p /config/www && tar xzf /backup/bookstack-files-2026-10-09.tar.gz -C /config/www && chown -R 1000:1000 /config'
docker compose up -d bookstack

الخيار --wait يجعل Compose ينتظر حتى يصبح فحص الصحة Healthcheck الخاص ب MariaDB ناجحاً، وchown يعطي الملفات للمستخدم 1000 الذي حددناه في PUID. وعند التشغيل كتب BookStack في سجله Nothing to migrate.، أي أنه وجد الجداول كاملة. ولأن النسخة الجديدة على عنوان مختلف شغلنا أمر تحديث العنوان الذي يذكره التوثيق:

docker compose exec bookstack php /app/www/artisan bookstack:update-url http://wiki.localhost:29401 http://wiki.localhost:29402
Updated 0 JSON encoded rows in settings->value
URL update procedure complete.
Be sure to run "php artisan cache:clear" to clear any old URLs in the cache.

ثم تحققنا عبر ال API في النسخة المستعادة من عدد الصفحات ومن محتوى المرفق الذي رفعناه قبل النسخ:

pages via API: 4
wg-sara.conf Address = 10.8.0.11/32

الصفحات الأربع موجودة، والمرفق عاد بمحتواه، والتوكن نفسه عمل لأنه محفوظ في قاعدة البيانات. وبعد ذلك انقل ملفات النسخة إلى مكان آخر كما في قاعدة 3-2-1.

التحديث إلى إصدار أحدث

تقول LinuxServer إن التحديث يكون بسحب Docker Image الجديدة وإعادة إنشاء ال Container، وليس بالتحديث داخله، ويطبق BookStack تغييرات قاعدة البيانات Migrations تلقائياً عند التشغيل. وقد بدأنا المختبر بالإصدار v26.09 ثم حدثناه إلى v26.09.1 بالخطوات التالية:

  1. اقرأ قسم Version Specific Instructions في صفحة التحديث لكل إصدار بين إصدارك والإصدار الجديد، ففيه التغييرات التي تكسر الإعداد مثل إلزام APP_KEY في v26.09، والثغرات الأمنية التي أصلحها كل إصدار، مثل مجموعة الثغرات التي أصلحها الإصداران v26.05.3 وv26.05.4.
  2. خذ نسخة احتياطية كما في القسم السابق.
  3. غير الوسم في .env من v26.09-ls286 إلى v26.09.1-ls287، ثم شغل docker compose pull وdocker compose up -d.
  4. تأكد من السجل ومن رقم الإصدار.

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

docker compose logs bookstack | grep -i 'migrat'
curl -s -H "$AUTH" $B/api/system
bookstack-1  | [migrations] done
bookstack-1  |    INFO  Nothing to migrate.
{"version":"v26.09.1","instance_id":"483ca879-0e4c-44a0-988c-2a6260e0e131","app_name":"BookStack","app_logo":"http:\/\/wiki.localhost:29401\/logo.png","base_url":"http:\/\/wiki.localhost:29401"}

لم تكن هناك تغييرات في قاعدة البيانات لأن v26.09.1 إصدار إصلاحات، وفيه بحسب صفحة الإصدار فحص لحجم ملفات ZIP قبل فكها عند الاستيراد، وإصلاح لصلاحيات الصفحات عند تحويل فصل إلى كتاب. أما عند الانتقال بين إصدارات كبيرة فسوف ترى أسطر DONE لكل تغيير كما في التشغيل الأول.

إعدادات تستحق الانتباه

في ملف .env.example.complete كل الخيارات، وهذه ثلاثة منها نوصي بالنظر فيها:

  • DISABLE_EXTERNAL_SERVICES=true: بشكل افتراضي يجلب BookStack الصور الشخصية من Gravatar ويستخدم diagrams.net للرسوم، وفي المختبر جلب السيرفر صورة لكل مستخدم جديد وحفظها في المجلد /config/www/uploads/images/user. وهذا الخيار يوقف الخدمتين، فلا يرسل السيرفر هاش بريد كل موظف إلى طرف خارجي ليجلب صورته.
  • AVATAR_URL: إذا أردت صوراً شخصية من خدمة تختارها أنت مثل Libravatar.
  • QUEUE_CONNECTION=database: يجعل إرسال البريد وال Webhooks في الخلفية، كما تذكر صفحة LinuxServer، فلا ينتظر المستخدم حتى يرسل البريد.

أدلة تكمل هذا المقال

هذا المقال جزء من مرحلة التوثيق والمعرفة في خريطة التعلم، وفيها مقالات قادمة عن Wiki.js وDocmost ثم مقارنة بينها وبين Outline وBookStack. وهذه الأدلة التي يعتمد عليها هذا المقال:

دليلOutline: قاعدة معرفة فريقك على سيرفرككيف تشغل Outline على سيرفرك ليكتب فيه فريقك إجراءاته وقراراته، ويدخل إليه الموظفون بحساباتهم في authentik.دليلتثبيت authentik للدخول الموحد SSO إلى تطبيقاتككيف تثبت authentik حتى يدخل كل موظف إلى تطبيقات الفريق بحساب واحد، مع فرض المصادقة الثنائية على الجميع.دليلشرح OAuth وOpenID Connect للمبتدئينلماذا لا يجب أن يعرف أي تطبيق كلمة مرورك، وكيف يحل OAuth ذلك بتوكنات محدودة، وكيف يضيف OpenID Connect الدخول الموحد.دليلكيف يرى تطبيقك عنوان الزائر الحقيقي خلف ال Reverse Proxyإذا كان تطبيقك يرى عنوان ال Proxy بدلاً من الزائر أو يولد روابط http، فهذا هو السبب والإعداد الصحيح لإطار عملك.دليلالنسخ الاحتياطي بقاعدة 3-2-1 واختبار الاستعادةكيف تحدد كم بيانات تقبل أن تخسر، وتبني نسخاً لا يستطيع المخترق حذفها، وتتأكد أنك تستطيع استعادتها فعلاً.

الخلاصة

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

  • BookStack يرتب التوثيق في أرفف وكتب وفصول وصفحات، ورخصته MIT، ويناسب التوثيق الذي له مالك وشكل ثابت ويحتاج إلى صلاحيات على مستوى الكتاب.
  • ثبته بال Docker Image من LinuxServer بوسم ثابت مع MariaDB، وولد APP_KEY واحفظه مع ملف .env، لأنه إلزامي منذ v26.09 ولأن فقده يفقدك بيانات MFA، وغير حساب المدير الافتراضي فوراً.
  • الواجهة والمحتوى بالعربية سليمان، ولكن الكود داخل السطر يحتاج إلى سطر CSS في التخصيص، وتصدير PDF الافتراضي لا يصلح للعربية، فاطبع ملف HTML من المتصفح.
  • البحث يطابق أول الكلمة كما كتبت، فلا يجد «سيرفرات» إذا كان النص «السيرفرات»، لذلك علم فريقك البحث بأول الكلمة واستخدم الوسوم للتصنيف.
  • الصلاحية على الكتاب تنتقل إلى ما تحته، وعلى الرف لا تنتقل، والمستخدم الممنوع لا يرى حتى أن الكتاب موجود.
  • مع authentik يصبح الدخول زراً واحداً وتأتي الأدوار من المجموعات، ولكن اربط حساب المدير قبل التغيير، وتأكد من أنواع المنح grant types في ال Provider.
  • انسخ قاعدة البيانات و/config/www وملفي Compose و.env، وعند الاستعادة شغل قاعدة البيانات واستورد النسخة قبل أن تشغل BookStack.

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

  • أكتوبر 2026: كتابة الدليل واختباره على BookStack v26.09 ثم v26.09.1 بال Docker Image lscr.io/linuxserver/bookstack (الوسمان v26.09-ls286 وv26.09.1-ls287، PHP 8.5.11) مع mariadb:11.8.9، والدخول الموحد عبر authentik 2026.8.3، على Docker Engine 29.8.0.
نشرة عرب رووت | ArabRoot

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

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

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

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