في دليل 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. وأهم ما يميزه أنه يفرض على التوثيق شكلاً واحداً بأربعة مستويات، وهذا ما تبينه الصورة التالية بالمحتوى الذي أنشأناه في المختبر:

- الرف 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 دون أي تعديل:

النص العربي والعناوين والجدول والقوائم المرقمة كلها سليمة، ولكن لاحظ السطر الثاني من الخطوات، فالمسار /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>والصورة التالية تبين الصفحة نفسها بعد الحفظ:

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

وتغيير الخط لا يحل هذه المشكلة، لأن المشكلة في ترتيب الكلمات وليس في شكل الحروف. وأمامك حلان: الأول أن تصدر الصفحة بصيغة Contained Web File، وهي ملف HTML واحد فيه الصور والتنسيق، ثم تطبعه إلى PDF من المتصفح، وقد فعلنا ذلك بمتصفح Chrome فخرجت الصفحة العربية سليمة. والثاني أن تجعل BookStack يستخدم برنامجاً آخر بالخيار EXPORT_PDF_COMMAND، ويحذر التوثيق نفسه من أن هذا الخيار يشغل برنامجاً خارجياً على HTML يكتبه المستخدمون، فيجب عزله عن الشبكة والملفات، ولم نجربه في هذا المقال.
البحث بالعربية
يدعم BookStack صيغة بحث متقدمة يشرحها توثيق البحث: الكلمات العادية، والعبارة الكاملة بين علامتي تنصيص، والوسوم Tags بين أقواس مربعة، والمرشحات Filters بين أقواس معقوفة. ولكن السؤال المهم لنا هو كيف يتعامل مع العربية، حيث تلتصق «ال» و«و» و«ل» بالكلمة. فأرسلنا مجموعة استعلامات إلى المسار /api/search على المحتوى الذي أنشأناه، وكانت النتائج كما يلي:
| الاستعلام | عدد النتائج | الملاحظة |
|---|---|---|
السيرفرات | 4 | الفصل والكتاب وصفحتان |
السيرف | 4 | جزء من أول الكلمة يكفي |
سيرفرات | 0 | الكلمة في النص هي «السيرفرات» و«للسيرفرات» |
سيرفر | 0 | السبب نفسه |
إجازة | 1 | موجودة في النص بهذا الشكل |
اجازة | 0 | لا يوحد البحث بين الهمزة والألف |
wireguard | 1 | البحث لا يفرق بين الحروف الكبيرة والصغيرة |
"المفتاح الخاص" | 1 | العبارة الكاملة |
[قسم=التقنية] | 2 | البحث بالوسم وقيمته |
{type:page} [قسم=التقنية] | 2 | الصفحات فقط ذات الوسم |
10.8.0.12 | 1 | عنوان داخل جدول |
والنتيجة أن البحث يطابق أول الكلمة كما كتبت في النص، فالكلمة «السيرف» تجد «السيرفرات»، ولكن «سيرفرات» لا تجدها لأن «ال» جزء من الكلمة عنده، ولا يوجد تحليل صرفي للعربية ولا توحيد للهمزات. والصورة التالية تبين صفحة النتائج في الواجهة:

لذلك اتفق مع فريقك على ثلاث عادات: ابحث بأول الكلمة دون آخرها، وجرب الكلمة مع «ال» ودونها، واستخدم الوسوم 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 في آخر الرد يعني أن بقية الأدوار تبقى على صلاحياتها الافتراضية. والصورة التالية تبين الصلاحية نفسها في شاشة صلاحيات الكتاب:

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

الدخول الموحد عبر 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:


دخلت المستخدمة سارة وهي في مجموعة Editor في authentik، فأنشأ BookStack حسابها تلقائياً، وأعاد الطلب GET /api/users/4 ما يلي بعد اختيار بعض الحقول:
سارة الأحمد [email protected] external_auth_id= 695de2b5fd088fca… roles= ['Editor']والقيمة external_auth_id هي sub القادمة من authentik، وهي التي يربط بها BookStack الحساب في كل دخول بعد ذلك. والصورة التالية تبين كتاب التقنية كما تراه سارة، ومعها أزرار إنشاء الصفحات والفصول التي يعطيها دور Editor:

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/markdown | 836 بايت | المحتوى كما كتب، والعربية سليمة |
| نص عادي | /api/pages/7/export/plaintext | 759 بايت | للبحث أو النقل إلى نظام آخر |
| Contained Web File | /api/pages/7/export/html | 21,536 بايت | ملف HTML واحد فيه التنسيق، ويطبع من المتصفح بشكل سليم |
/api/pages/7/export/pdf | 1,132,793 بايت | dompdf، والعربية بترتيب معكوس كما رأينا | |
| Portable ZIP | /api/books/2/export/zip | 1,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:29402Updated 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 بالخطوات التالية:
- اقرأ قسم Version Specific Instructions في صفحة التحديث لكل إصدار بين إصدارك والإصدار الجديد، ففيه التغييرات التي تكسر الإعداد مثل إلزام APP_KEY في v26.09، والثغرات الأمنية التي أصلحها كل إصدار، مثل مجموعة الثغرات التي أصلحها الإصداران v26.05.3 وv26.05.4.
- خذ نسخة احتياطية كما في القسم السابق.
- غير الوسم في
.envمنv26.09-ls286إلىv26.09.1-ls287، ثم شغلdocker compose pullوdocker compose up -d. - تأكد من السجل ومن رقم الإصدار.
وفي المختبر كان المخرج كما يلي:
docker compose logs bookstack | grep -i 'migrat'
curl -s -H "$AUTH" $B/api/systembookstack-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.