في مقارنة أدوات رسم المخططات وجدنا أن Excalidraw أنسب أداة لرسم النقاش مع الفريق، ووجدنا أيضاً ملاحظة مهمة: الـ Docker Image الرسمية تعمل على سيرفرك، ولكن الرسم المشترك فيها يمر بخادم الشركة، وحفظ الغرف يذهب إلى Firebase، والخطوط تأتي من CDN خارجي، وفيها سكربت تحليلات. وإذا كنت ترسم على هذه اللوحة شبكة شركتك الداخلية أو تصميم نظام لم يعلن بعد، فأنت تريد أن يبقى كل ذلك على سيرفرك.
المواضيع في السلسلة:
- المخططات التقنية Diagrams: ماذا ترسم ومتى؟
- أدوات رسم المخططات Diagrams: مقارنة عملية
- Excalidraw على سيرفرك مع الرسم المشترك (الموضوع الحالي)
وفي هذا الدليل سوف نقوم بما يلي:
- نفحص الـ Image الرسمية ونرى بالأوامر إلى أين تتصل.
- نبني Excalidraw من الكود بعنوان خادم الغرف الخاص بنا وبدون Firebase.
- نزيل الاتصال بالـ CDN وسكربت التحليلات، ثم نشغل الواجهة وخادم الغرف excalidraw-room خلف Caddy على نطاق واحد.
- نرسم من متصفحين في نفس الغرفة، ونكتب نصاً عربياً، ونصدر الرسم إلى PNG وSVG، ونرى في سجلات الخادم أنه لا يقرأ ما نرسم.
- نجرب ماذا يحدث عندما يخرج الجميع من الغرفة، وماذا يحدث إذا توقف خادم الغرف.
- نشغل draw.io بجانبه، ونحول مخطط Mermaid إلى صورة بأداة mermaid-cli.
والصورة التالية تبين ما سوف نبنيه:

المتطلبات
- سيرفر Linux عليه Docker وDocker Compose، وقد اختبرنا على Docker Engine 29.8.0.
- البناء يتم داخل Docker، فيثبت Node.js والحزم ويبني الواجهة، ويأخذ عدة دقائق ويحتاج ذاكرة أكبر من التشغيل، فإذا كان السيرفر صغيراً فابن الـ Image على جهازك ثم انقلها إليه. أما بعد التشغيل فالـ Containers صغيرة: الواجهة استهلكت حوالي 8 ميجابايت وخادم الغرف حوالي 39 ميجابايت من الذاكرة في تجربتنا.
- Git لنسخ الكود، واتصال بالإنترنت وقت البناء لتنزيل الحزم.
- نطاق فرعي Subdomain مثل
draw.example.comيشير إلى السيرفر، والمنفذان 80 و443 مفتوحان، إذا أردت الوصول من خارج جهازك.
لماذا لا تكفي الـ Image الرسمية؟
الحل البسيط الذي يخطر على البال هو تشغيل excalidraw/excalidraw مباشرة، وهي تعمل فعلاً وتفتح اللوحة، ولكن لنر ما الذي بداخلها. أولاً لاحظ أنها لا تحمل إلا الوسم latest، فلا يوجد رقم إصدار تثبته، لذلك نثبت الـ Digest، ثم نبحث في ملفات الواجهة عن العناوين الخارجية:
docker image inspect excalidraw/excalidraw:latest --format '{{index .RepoDigests 0}} {{.Created}}'
docker run --rm --entrypoint sh excalidraw/excalidraw:latest -c \
"cd /usr/share/nginx/html && grep -hoE 'https://(oss-collab\.excalidraw\.com|[a-z-]+\.firebaseio\.com|scripts\.simpleanalyticscdn\.com|excalidraw\.nyc3\.cdn\.digitaloceanspaces\.com)' index.html assets/*.js | sort | uniq -c"والمخرج سوف يكون كما يلي:
excalidraw/excalidraw@sha256:f7ee194addd607bf831d2af0f0a34463dd4225e426cf35199ef0b12a803398e9 2026-05-06T21:19:58.716188897Z
3 https://excalidraw-room-persistence.firebaseio.com
5 https://excalidraw.nyc3.cdn.digitaloceanspaces.com
2 https://oss-collab.excalidraw.com
1 https://scripts.simpleanalyticscdn.comفي المخرج أعلاه لاحظ التالي:
oss-collab.excalidraw.comهو خادم الغرف الذي تتصل به الواجهة عند بدء الرسم المشترك، فحتى لو كانت الواجهة على سيرفرك فالجلسات المشتركة تمر بخادم الشركة، صحيح أنها مشفرة كما سيأتي، ولكن عنوان الغرفة ووقت الجلسة يصلان إليه.firebaseio.comهو مكان حفظ الغرف والملفات المرفقة في نسخة excalidraw.com.digitaloceanspaces.comهو الـ CDN الذي تحمل منه الخطوط، وsimpleanalyticscdn.comسكربت التحليلات.
والسبب أن هذه العناوين لا تتغير بمتغيرات بيئة Environment Variables عند تشغيل الـ Container، لأن Excalidraw تطبيق Vite، والقيم في ملف .env.production تكتب داخل ملفات JavaScript وقت البناء، والـ Dockerfile الرسمي يبني التطبيق ثم ينسخ الملفات الناتجة إلى nginx فقط. لذلك الحل الأنسب هو أن نبني الـ Image بأنفسنا بالقيم التي نريدها.
بناء Excalidraw من الكود
الآن سوف نقوم بنسخ الكود من فرع release، وهو الفرع الذي يبنى منه النشر الرسمي، ونثبت رقم الـ Commit الذي اختبرناه لكي يكون البناء قابلاً للتكرار:
mkdir -p /opt/draw && cd /opt/draw
git clone --branch release https://github.com/excalidraw/excalidraw.git
cd excalidraw
git checkout 17d8904f734464847bcef638b636647b85ac1a1dبعد ذلك نعدل سطرين في .env.production: عنوان خادم الغرف ليصبح نطاقنا نفسه، وإعدادات Firebase لتصبح فارغة:
sed -i \
-e 's#^VITE_APP_WS_SERVER_URL=.*#VITE_APP_WS_SERVER_URL=https://draw.example.com#' \
-e "s#^VITE_APP_FIREBASE_CONFIG=.*#VITE_APP_FIREBASE_CONFIG='{}'#" \
.env.production
grep -n "WS_SERVER\|FIREBASE" .env.production15:VITE_APP_WS_SERVER_URL=https://draw.example.com
17:VITE_APP_FIREBASE_CONFIG='{}'في الإعداد أعلاه لاحظ التالي:
- العنوان هو العنوان الذي يكتبه المستخدم في متصفحه، وليس اسم الـ Container، لأن الاتصال بخادم الغرف يتم من المتصفح وليس من السيرفر. وقد اختبرنا البناء بالعنوان
http://localhost:18704على جهاز التجربة، فضع أنت نطاقك. - جعلنا خادم الغرف على نفس النطاق، ومكتبة Socket.IO تتصل افتراضياً بالمسار
/socket.io/، فيكفي أن يوجه الـ Reverse Proxy هذا المسار إلى excalidraw-room، ولا نحتاج نطاقاً فرعياً ثانياً ولا شهادة ثانية. - القيمة
'{}'تعني أنه لا يوجد Firebase، وسوف نرى أثر ذلك في قسم ماذا يحدث عندما يخرج الجميع.
الآن نبني الـ Image:
docker build -t excalidraw-local:17d8904 .وعند تشغيل الأمر سوف تجد أنه يأخذ عدة دقائق، فقد استغرق البناء الأول في تجربتنا 4 دقائق و39 ثانية، منها 160 ثانية لتثبيت الحزم بـ yarn و47 ثانية لبناء الواجهة بـ Vite، وآخر المخرج يبين نجاح البناء:
#13 49.57 ✓ built in 46.83s
#13 DONE 50.0sوالـ Image الناتجة حجمها 92 ميجابايت، لأنها nginx وملفات ثابتة فقط، أما Node.js والحزم فتبقى في مرحلة البناء Build Stage ولا تدخل الـ Image النهائية.
إزالة الـ CDN وسكربت التحليلات Analytics
بقي عنوانان لا يأتيان من .env.production، وإنما من ملف index.html نفسه: سكربت التحليلات الذي يضاف في كل بناء Production، ومسار الخطوط EXCALIDRAW_ASSET_PATH الذي يضع الـ CDN أولاً ثم السيرفر نفسه. ولكي لا نعدل الكود نفسه، نضيف طبقة صغيرة فوق الـ Image التي بنيناها، في ملف /opt/draw/Dockerfile.local:
FROM excalidraw-local:17d8904
RUN sed -i -e 's#"https://excalidraw.nyc3.cdn.digitaloceanspaces.com/oss/",##' \
-e 's#https://scripts.simpleanalyticscdn.com/latest.js#/no-analytics.js#' \
/usr/share/nginx/html/index.htmlوالسطر الأول يحذف الـ CDN من قائمة مسارات الخطوط فتحمل من السيرفر نفسه، والثاني يغير عنوان سكربت التحليلات إلى ملف غير موجود على سيرفرك، فيفشل تحميله بصمت ولا يخرج أي طلب. وبعد البناء فحصنا الـ Image الجديدة بنفس أمر grep فكان المخرج:
4 https://excalidraw.nyc3.cdn.digitaloceanspaces.comواختفت عناوين Firebase وخادم الغرف والتحليلات، والأربعة الباقية روابط تحميل مسبق Preload لأربعة خطوط في رأس الصفحة، فإذا كان السيرفر لا يصل إلى الإنترنت فسوف يفشل هذا التحميل المسبق، ويستخدم الرسم خطوطه من السيرفر، وهذا ما رأيناه فعلاً عند التجربة مع حجب كل العناوين الخارجية كما سيأتي.
sed في Dockerfile.local يعتمد على نص محدد في index.html، فإذا تغير هذا النص في إصدار قادم فلن يفشل البناء، وإنما سوف يمر الأمر دون أن يغير شيئاً. لذلك بعد كل تحديث أعد تشغيل أمر grep أعلاه على الـ Image الجديدة، ولا تعتمد على أن البناء نجح.التشغيل خلف Caddy
نحتاج ثلاث خدمات: الواجهة التي بنيناها، وخادم الغرف excalidraw-room، وCaddy كـ Reverse Proxy يوجه المسار /socket.io/ إلى خادم الغرف وكل ما عداه إلى الواجهة. وخادم الغرف ليس له إلا وسوم بأرقام Commit، وآخرها sha-03ff435 المبنية في 15 ديسمبر 2023، فنثبت الـ Digest الخاص بها. أنشئ الملف /opt/draw/compose.yaml:
services:
excalidraw:
build:
context: .
dockerfile: Dockerfile.local
image: excalidraw-local:17d8904-local
restart: unless-stopped
room:
image: excalidraw/excalidraw-room@sha256:2fe999f9be4379e3ee282fc45d75d84a691a6383dde33544514cc395287c7a70
restart: unless-stopped
proxy:
image: caddy:2.11.7-alpine
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy-data:/data
depends_on: [excalidraw, room]
volumes:
caddy-data:ثم ملف /opt/draw/Caddyfile، ويستخدم التوجيه reverse_proxy بحسب المسار:
draw.example.com {
handle /socket.io/* {
reverse_proxy room:80
}
handle {
reverse_proxy excalidraw:80
}
}ثم نشغل الخدمات:
cd /opt/draw
docker compose up -d --buildفي الإعداد أعلاه لاحظ التالي:
- Caddy يصدر شهادة TLS للنطاق تلقائياً عندما يكون النطاق يشير إلى السيرفر والمنفذان 80 و443 مفتوحين، وقد شرحنا ذلك بالتفصيل في دليل Caddy. وفي تجربتنا على جهاز محلي استخدمنا
:80بدل اسم النطاق، ونشرنا المنفذ على127.0.0.1فقط. - الواجهة وخادم الغرف لا ينشران أي منفذ على السيرفر، فلا يصل إليهما أحد إلا عبر Caddy.
- Caddy يمرر اتصال WebSocket دون إعداد إضافي، وهذا ما يحتاجه Socket.IO.
- الـ Volume الوحيد هو بيانات Caddy التي فيها الشهادات، أما Excalidraw وخادم الغرف فلا يحفظان شيئاً على القرص.
وللتحقق من أن المسارين يعملان، نطلب الصفحة الرئيسية، ثم نطلب Socket.IO بطريقة Polling:
curl -s -o /dev/null -w "%{http_code}\n" https://draw.example.com/
curl -s "https://draw.example.com/socket.io/?EIO=4&transport=polling"والمخرج في تجربتنا على localhost:
200
0{"sid":"gKJq4dV1MAUurHmgAAAA","upgrades":["websocket"],"pingInterval":25000,"pingTimeout":20000,"maxPayload":1000000}والسطر الثاني رد من خادم الغرف نفسه، يعطي رقم جلسة ويعرض الترقية إلى WebSocket، فإذا ظهر بدلاً منه صفحة HTML فالمسار /socket.io/ يذهب إلى الواجهة وليس إلى خادم الغرف.
الرسم المشترك Real-time Collaboration من متصفحين
افتح https://draw.example.com، ثم اضغط زر المشاركة الأخضر في الأعلى، ثم Start session، فيعطيك Excalidraw رابطاً بالشكل التالي:
https://draw.example.com/#room=26dcc716436cbd8d8277,<KEY>والجزء بعد #room= فيه قيمتان: رقم الغرفة Room ID، ومفتاح التشفير Key. وأي شيء بعد علامة # في الرابط لا يرسله المتصفح إلى السيرفر أصلاً، لذلك يبقى المفتاح في المتصفح، ويرسل المتصفح لخادم الغرف رقم الغرفة فقط ومعه بيانات مشفرة بخوارزمية AES-GCM بمفتاح 128 بت، كما يشرح مقال الفريق عن التشفير، وكما وجدنا في كود المشروع.

وعندما ضغطنا Start session ظهرت لنا رسالة خطأ فوق نافذة المشاركة:

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

ولكي نتأكد من أن خادم الغرف لا يرى الرسم، شغلناه مرة بمتغير البيئة DEBUG='*' ليطبع كل ما يصله، وهذا جزء من السجل أثناء الرسم:
socket.io:socket got packet {"type":2,"nsp":"/","data":["server-volatile-broadcast","a33384c03fca8fc3861e",{"type":"Buffer","data":[93,177,128,180,246,191,86,71,6,240,114,156,1,174,66,22,234,28,195,61,...]},{"type":"Buffer","data":[161,111,233,172,237,80,72,56,245,21,63,22]}]}
socket A5KueQzGHCYx_7J5AAAD sends volatile update to a33384c03fca8fc3861eفي السجل أعلاه لاحظ التالي:
- القيمة
a33384c03fca8fc3861eهي رقم الغرفة، وهي الشيء الوحيد المقروء في الرسالة. - الـ Buffer الأول هو البيانات المشفرة، والثاني طوله 12 بايت، وهو الـ IV العشوائي الذي يولده المتصفح لكل رسالة.
- لا يظهر في السجل أي نص كتبناه ولا أي إحداثيات، لذلك فمن يصل إلى سجلات السيرفر لا يعرف ماذا رسمنا، ولكنه يعرف متى كانت الغرفة نشطة وكم شخصاً فيها.
النص العربي واتجاهه RTL
كتبنا في المتصفح الأول «سيرفر الإنتاج Production Server»، وفي الثاني «قاعدة البيانات Database»، وكما ترى في الصورة السابقة اكتشفت الأداة أن النص يبدأ بالعربية فجعلت اتجاهه من اليمين إلى اليسار، فظهرت الكلمة العربية أولاً للقارئ، وظهرت الحروف متصلة بشكل صحيح. والملاحظة الوحيدة أن العربية تظهر بخط النظام وليس بالخط الذي يشبه اليد Excalifont، لأن هذا الخط لا يحتوي الحروف العربية، فيبدو النص العربي أكثر رسمية من بقية الرسم، وهذا لا يؤثر على القراءة.
التصدير Export إلى PNG وSVG
من القائمة، أو بالاختصار Ctrl+Shift+E، تفتح نافذة تصدير الصورة Export image:

في النافذة أعلاه لاحظ التالي:
- Only selected يكون مفعلاً إذا كان هناك عنصر محدد، فإذا صدرت والنص محدد فسوف تحصل على صورة فيها النص وحده، وهذا ما حدث معنا في المحاولة الأولى، لذلك اضغط على مكان فارغ قبل فتح النافذة.
- Embed scene يضع بيانات الرسم داخل ملف PNG أو SVG، فإذا فتحت الملف لاحقاً في Excalidraw عاد قابلاً للتعديل، وهو الخيار الذي ننصح به للصور التي تضعها في المستودع.
- Scale يحدد دقة الصورة، و2x مناسبة لصفحات التوثيق.
وبعد إلغاء التحديد صدرنا اللوحة كاملة، فكان ملف PNG بحجم 27 كيلوبايت وأبعاد 699 في 243، وملف SVG بحجم 8 كيلوبايت، وهذه الصورة كما خرجت:

وفي ملف SVG وجدنا أن النص العربي محفوظ كنص وليس كأشكال، ومعه اتجاهه:
<text x="0" y="17.619999999999997" font-family="Excalifont, Xiaolai, sans-serif, Segoe UI Emoji" font-size="20px" fill="#1e1e1e" text-anchor="end" style="white-space: pre;" direction="rtl" dominant-baseline="alphabetic">سيرفر الإنتاج Production Server</text>وهذا يعني أن النص في الصورة قابل للبحث والنسخ، وأن المتصفح الذي يعرض الصورة هو الذي يختار الخط العربي، فقد يختلف شكل الخط قليلاً بين جهاز وآخر.
ماذا يحدث عندما يخرج الجميع؟
خادم الغرف لا يحفظ شيئاً، فهو يمرر الرسائل بين من هم في الغرفة فقط، والمتصفح الذي يدخل الغرفة يطلب الرسم من المتصفحات الموجودة فيها. لذلك جربنا ثلاث خطوات: رسم المتصفح الأول مستطيلاً، ثم دخل الثاني فرأى المستطيل، ثم أغلقنا المتصفحين، وبعدها فتحنا نفس الرابط في متصفح ثالث:

وهذا هو أثر إزالة Firebase، ففي excalidraw.com تحفظ الغرفة مشفرة في Firebase فتعود عندما يفتح أحدهم الرابط لاحقاً، أما في بنائنا فالغرفة موجودة ما دام فيها شخص واحد على الأقل. والحل البسيط أن يحفظ أحد المشاركين الرسم في نهاية الجلسة من القائمة Save to، أو بالاختصار Ctrl+S، كملف .excalidraw ويضعه في المستودع بجانب الكود، أو يصدره كـ PNG مع Embed scene. وإذا كنت تحتاج حفظاً دائماً للغرف ومساحة مشتركة للفريق فهذا ما تبيعه Excalidraw+، أو تستطيع أن تربط بناءك بمشروع Firebase تملكه أنت، وهذا خارج نطاق هذا الدليل.
draw.io بجانبه
إذا كان فريقك يحتاج أيضاً مخطط شبكة مفصل بأيقونات السحابة والأجهزة، فـ draw.io مناسب أكثر، وتشغيله أبسط من Excalidraw لأنه لا يحتاج بناء:
docker run -d --name drawio --restart unless-stopped -p 127.0.0.1:8081:8080 jgraph/drawio:32.5.5
curl -s -o /dev/null -w "%{http_code}\n" http://localhost:8081/200وظهرت الواجهة بالعربية عندما فتحنا http://localhost:8081/?lang=ar، ورسمنا فيها نفس المخطط مع ضبط اتجاه النص textDirection=rtl لكل شكل:

ولاحظ أمرين قبل أن تعتمد عليه: الأول أن هذه الـ Image تعطل المزامنة في الوقت نفسه، ووجدنا ذلك في ملف الإعداد داخل الـ Container:
docker exec drawio grep "sync" /usr/local/tomcat/webapps/draw/js/PreConfig.jsurlParams['sync'] = 'manual'; //Disable Real-Timeوالثاني أن ملف README في المستودع يقول إن هذه النسخة للتجربة والتقييم، وإن هناك نسخة إنتاج مدفوعة، لذلك استخدم draw.io المستضاف للرسم الفردي، واحفظ الملفات بصيغة .drawio.svg في المستودع، واجعل Excalidraw للجلسات المشتركة. ولكي تضعه خلف نفس الـ Reverse Proxy أضف له نطاقاً فرعياً مثل diagrams.example.com بنفس طريقة Caddyfile أعلاه.
مخطط Mermaid من سطر الأوامر CLI
وأخيراً، المخططات التي تعيش داخل المستودع ككود لا تحتاج سيرفراً أصلاً، فـ GitHub وGitLab يعرضان Mermaid مباشرة، ولكن إذا أردت صورة لتضعها في عرض تقديمي أو في Wiki لا يعرض Mermaid، فأداة mermaid-cli تحولها. وهذا ملف flow-rtl.mmd بتسميات عربية مع dir='rtl' الذي وجدناه ضرورياً في اختبار العربية:
flowchart LR
user["<span dir='rtl'>المستخدم User</span>"] -->|<span dir='rtl'>طلب HTTPS</span>| api["<span dir='rtl'>الخادم API Server</span>"]
api -->|SQL| db[("<span dir='rtl'>قاعدة البيانات Database</span>")]docker run --rm -v "$PWD":/data minlag/mermaid-cli:12.0.1 -i flow-rtl.mmd -o flow-rtl.png -s 2 -b whiteGenerating single mermaid chart
ولاحظ أن الـ Image حجمها 3.53 جيجابايت لأنها تحمل متصفح Chromium، وأن كل تحويل يأخذ حوالي 15 ثانية، فإذا كنت تولد عشرات المخططات في خط CI فحولها كلها في تشغيل واحد بدل تشغيل الـ Container لكل ملف.
التحقق من النجاح
curlعلى الصفحة الرئيسية يرجع 200، وعلى/socket.io/?EIO=4&transport=pollingيرجع سطراً يبدأ بـ0{"sid":.- أمر grep على الـ Image التي بنيتها لا يظهر فيه
oss-collab.excalidraw.comولاfirebaseio.comولاsimpleanalyticscdn.com. - بعد Start session يظهر على زر المشاركة رقم المشاركين، وعندما يدخل شخص ثان يصبح الرقم 2 ويظهر مؤشره باسمه.
- جربنا أيضاً حجب كل العناوين الخارجية عن المتصفحين، ففتحت اللوحة وعمل الرسم المشترك والتصدير، وهذا يعني أن الأداة تعمل في شبكة داخلية لا تصل إلى الإنترنت.
النسخ الاحتياطي Backup والاستعادة Restore
لا توجد بيانات على السيرفر تحتاج نسخاً احتياطياً Backup، لأن خادم الغرف لا يحفظ شيئاً والواجهة ملفات ثابتة، لذلك فالذي تحفظه هو ما تحتاجه لإعادة البناء:
- المجلد
/opt/drawبدون مجلد الكود:compose.yamlوCaddyfileوDockerfile.local، ورقم الـ Commit الذي بنيت منه، والأفضل أن تضعها في مستودع Git خاص. - الـ Volume الخاص بـ Caddy إذا أردت ألا يطلب شهادات جديدة بعد الاستعادة.
- والأهم من كل ذلك ملفات
.excalidrawالتي يحفظها الفريق، ومكانها المستودعات نفسها، فإذا ضاع السيرفر بقيت المخططات.
cd /opt/draw
tar czf /backup/draw-config-$(date +%F).tgz compose.yaml Caddyfile Dockerfile.local
git -C excalidraw rev-parse HEAD > /backup/draw-commit.txtالتحديث Upgrade إلى إصدار أحدث
التحديث يعني بناء الـ Image من Commit أحدث في فرع release، ولأن ملف .env.production قد يتغير بين الإصدارات فلا تنسخ ملفك القديم فوقه، وإنما أعد تطبيق أمر sed على الملف الجديد:
cd /opt/draw/excalidraw
git checkout -- .env.production
git fetch origin release
git checkout origin/release
git log -1 --format='%H %cI %s'
sed -i \
-e 's#^VITE_APP_WS_SERVER_URL=.*#VITE_APP_WS_SERVER_URL=https://draw.example.com#' \
-e "s#^VITE_APP_FIREBASE_CONFIG=.*#VITE_APP_FIREBASE_CONFIG='{}'#" \
.env.production
docker build -t excalidraw-local:new .بعد ذلك غير الوسم في Dockerfile.local وcompose.yaml إلى الوسم الجديد، وشغل docker compose up -d --build، ثم أعد أمر grep وتجربة المتصفحين قبل أن تعلن التحديث للفريق. وخادم الغرف لم يتغير منذ ديسمبر 2023، ويعمل على Node.js 12.22.12 الذي انتهى دعمه، لذلك أبقه داخل الشبكة الداخلية خلف Caddy كما فعلنا، ولا تنشر منفذه مباشرة.
مشكلات شائعة وحلولها
ماذا يحدث إذا توقف خادم الغرف Room Server؟
نجرب العطل: أوقفنا Container خادم الغرف، ثم فتحنا رابط غرفة جديدة، فلم تظهر أي رسالة خطأ، وفتحت اللوحة كأنها لوحة عادية، وكل ما تغير أن زر المشاركة ليس عليه رقم:

وهذه أخطر حالة، لأن كل شخص يظن أنه في جلسة مشتركة وهو يرسم وحده، لذلك إذا قال أحد الزملاء إنه لا يرى ما ترسمه فانظر أولاً إلى الرقم على زر المشاركة، ثم شغل أمر curl على /socket.io/، ثم docker compose logs room.
الرسم المشترك لا يعمل بعد نقل السيرفر إلى نطاق آخر
عنوان خادم الغرف مكتوب داخل ملفات الواجهة وقت البناء، فإذا غيرت النطاق فأعد البناء بالعنوان الجديد، ولا تكتف بتغيير Caddyfile، والسبب أن المتصفح سوف يستمر في الاتصال بالعنوان القديم المكتوب في ملفات الواجهة. وللتأكد من العنوان الموجود في الـ Image شغل grep على assets/*.js بحثاً عن نطاقك.
رسالة «Couldn't save to the backend database»
تظهر عند بدء الجلسة لأن Firebase فارغ، وهي متوقعة في هذا الإعداد ولا تعني أن الجلسة فشلت، وتذكير بأن الرسم لا يحفظ على السيرفر.
خط النوافذ Font يظهر مختلفاً في شبكة مغلقة
إذا كان المتصفح لا يصل إلى الإنترنت فسوف تظهر بعض نصوص النوافذ بخط Serif بدل خط الواجهة، والسبب أن ملف CSS يطلب خط الواجهة Assistant من الـ CDN، أما خطوط الرسم نفسها فتحمل من السيرفر، والرسم والتصدير لا يتأثران.
إزالة التجربة
إذا كنت جربت على جهازك وتريد إزالة كل شيء:
cd /opt/draw
docker compose down -v
docker rm -f drawio
docker rmi excalidraw-local:17d8904 excalidraw-local:17d8904-local caddy:2.11.7-alpine \
excalidraw/excalidraw-room@sha256:2fe999f9be4379e3ee282fc45d75d84a691a6383dde33544514cc395287c7a70 \
jgraph/drawio:32.5.5 minlag/mermaid-cli:12.0.1الخلاصة
وصلنا لنهاية الموضوع، وأهم ما فيه:
- الـ Image الرسمية لـ Excalidraw تعمل على سيرفرك، ولكن الرسم المشترك فيها يتصل بخادم الشركة، والحفظ يذهب إلى Firebase، والخطوط من CDN، وفيها سكربت تحليلات.
- هذه العناوين تكتب وقت البناء، لذلك نبني الـ Image بأنفسنا بعد تعديل سطرين في
.env.production، ثم نزيل الـ CDN والتحليلات بطبقة صغيرة فوقها. - خادم الغرف وراء نفس النطاق على المسار
/socket.io/، فيكفي نطاق واحد وشهادة واحدة. - الرسم المشترك مشفر، والمفتاح في الرابط بعد
#، وسجلات الخادم لا تظهر فيها إلا أرقام الغرف وبيانات مشفرة، والرابط نفسه هو الصلاحية الوحيدة. - بدون Firebase تختفي الغرفة عندما يخرج آخر شخص، فاحفظ الرسم كملف
.excalidrawأو PNG مع Embed scene في المستودع. - العربية تعمل وتأخذ اتجاهها تلقائياً بخط النظام، وdraw.io مناسب بجانبه للمخطط المفصل بدون تعديل مشترك، وmermaid-cli يحول مخططات الكود إلى صور.
وأقرب المقالات إلى هذا الموضوع على الموقع:
دليلأدوات رسم المخططات Diagrams: مقارنة عمليةمقارنة أدوات الرسم مفتوحة المصدر والتجارية بالترخيص والاستضافة والرسم المشترك وGit، مع اختبار فعلي للعربية.
دليلالمخططات التقنية Diagrams: ماذا ترسم ومتى؟كيف تختار نوع المخطط من سؤال القارئ، ومتى ترسم باليد أو بالكود، وكيف تبقيه صحيحاً ومقروءاً بالعربية.
دليلتثبيت Caddy على خادمك: Reverse Proxy بشهادات HTTPS تلقائيةكيف تثبت Caddy على سيرفرك ليمنح كل موقع شهادة HTTPS تلقائياً، وتربط به تطبيقاتك بملف إعداد من بضعة أسطر.
دليلOutline: قاعدة معرفة فريقك على سيرفرككيف تشغل Outline على سيرفرك ليكتب فيه فريقك إجراءاته وقراراته، ويدخل إليه الموظفون بحساباتهم في authentik.سجل التحديثات
- أكتوبر 2026: كتابة الدليل واختباره على Excalidraw من فرع release (17d8904) مع excalidraw/excalidraw-room:sha-03ff435 وcaddy:2.11.7-alpine وjgraph/drawio:32.5.5 وminlag/mermaid-cli:12.0.1 على Docker Engine 29.8.0.