> ## Content Index
> Fetch the complete content index at: https://arabroot.io/llms.txt
> Use this file to discover other available public pages before exploring further.

# Excalidraw على سيرفرك مع الرسم المشترك
- URL: https://arabroot.io/articles/excalidraw-على-سيرفرك-مع-الرسم-المشترك/
- Published: 2026-10-23T06:20:00.000Z
- Updated: 2026-10-23T15:09:36.000Z
- Description: كيف تبني Excalidraw وخادم الغرف على سيرفرك لترسم مع فريقك في نفس اللوحة دون أن يمر رسمك بخدمة خارجية.
- Author: فريق عرب رووت
- Tags: الأدلة التقنية, الاستضافة الذاتية والتطبيقات, تطبيقات لفريقك, الاستضافة الذاتية, Excalidraw

في [مقارنة أدوات رسم المخططات](https://arabroot.io/articles/%D8%A7%D8%AF%D9%88%D8%A7%D8%AA-%D8%B1%D8%B3%D9%85-%D8%A7%D9%84%D9%85%D8%AE%D8%B7%D8%B7%D8%A7%D8%AA-diagrams-%D9%85%D9%82%D8%A7%D8%B1%D9%86%D8%A9-%D8%B9%D9%85%D9%84%D9%8A%D8%A9/) وجدنا أن Excalidraw أنسب أداة لرسم النقاش مع الفريق، ووجدنا أيضاً ملاحظة مهمة: الـ Docker Image الرسمية تعمل على سيرفرك، ولكن الرسم المشترك فيها يمر بخادم الشركة، وحفظ الغرف يذهب إلى Firebase، والخطوط تأتي من CDN خارجي، وفيها سكربت تحليلات. وإذا كنت ترسم على هذه اللوحة شبكة شركتك الداخلية أو تصميم نظام لم يعلن بعد، فأنت تريد أن يبقى كل ذلك على سيرفرك.

المواضيع في السلسلة:

- [المخططات التقنية Diagrams: ماذا ترسم ومتى؟](https://arabroot.io/articles/%D8%A7%D9%84%D9%85%D8%AE%D8%B7%D8%B7%D8%A7%D8%AA-%D8%A7%D9%84%D8%AA%D9%82%D9%86%D9%8A%D8%A9-diagrams-%D9%85%D8%A7%D8%B0%D8%A7-%D8%AA%D8%B1%D8%B3%D9%85-%D9%88%D9%85%D8%AA%D9%89/)
- [أدوات رسم المخططات Diagrams: مقارنة عملية](https://arabroot.io/articles/%D8%A7%D8%AF%D9%88%D8%A7%D8%AA-%D8%B1%D8%B3%D9%85-%D8%A7%D9%84%D9%85%D8%AE%D8%B7%D8%B7%D8%A7%D8%AA-diagrams-%D9%85%D9%82%D8%A7%D8%B1%D9%86%D8%A9-%D8%B9%D9%85%D9%84%D9%8A%D8%A9/)
- Excalidraw على سيرفرك مع الرسم المشترك (الموضوع الحالي)

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

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

والصورة التالية تبين ما سوف نبنيه:

![متصفحان A وB يتصلان بـ Caddy على draw.example.com، والمسار / يذهب إلى Container excalidraw فيه nginx وملفات الواجهة، والمسار /socket.io/ يذهب إلى excalidraw-room خادم الرسم المشترك الذي يعيد بث الرسائل المشفرة ولا يحفظ شيئاً، وفي الأسفل الخدمات التي تتصل بها الـ Image الرسمية وعليها علامة إلغاء: oss-collab.excalidraw.com وFirebase وfonts CDN وsimpleanalyticscdn.com](https://arabroot.io/content/images/2026/10/excalidraw-lab-01-lab-map.webp)

ثلاث Containers خلف نطاق واحد، والخدمات الخارجية في الأسفل هي ما نزيله في البناء الخاص بنا

## المتطلبات

- سيرفر 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](https://hub.docker.com/r/excalidraw/excalidraw?ref=arabroot.io) مباشرة، وهي تعمل فعلاً وتفتح اللوحة، ولكن لنر ما الذي بداخلها. أولاً لاحظ أنها لا تحمل إلا الوسم `latest`، فلا يوجد رقم إصدار تثبته، لذلك نثبت الـ Digest، ثم نبحث في ملفات الواجهة عن العناوين الخارجية:

```bash
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"
```

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

```text
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](https://github.com/excalidraw/excalidraw/blob/release/.env.production?ref=arabroot.io) تكتب داخل ملفات JavaScript وقت البناء، والـ [Dockerfile الرسمي](https://github.com/excalidraw/excalidraw/blob/release/Dockerfile?ref=arabroot.io) يبني التطبيق ثم ينسخ الملفات الناتجة إلى nginx فقط. لذلك الحل الأنسب هو أن نبني الـ Image بأنفسنا بالقيم التي نريدها.

## بناء Excalidraw من الكود

الآن سوف نقوم بنسخ الكود من فرع `release`، وهو الفرع الذي يبنى منه النشر الرسمي، ونثبت رقم الـ Commit الذي اختبرناه لكي يكون البناء قابلاً للتكرار:

```bash
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 لتصبح فارغة:

```bash
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.production
```

```text
15: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، وسوف نرى أثر ذلك في قسم [ماذا يحدث عندما يخرج الجميع](#everyone-leaves).

الآن نبني الـ Image:

```bash
docker build -t excalidraw-local:17d8904 .
```

وعند تشغيل الأمر سوف تجد أنه يأخذ عدة دقائق، فقد استغرق البناء الأول في تجربتنا 4 دقائق و39 ثانية، منها 160 ثانية لتثبيت الحزم بـ yarn و47 ثانية لبناء الواجهة بـ Vite، وآخر المخرج يبين نجاح البناء:

```text
#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](https://github.com/excalidraw/excalidraw/blob/release/excalidraw-app/index.html?ref=arabroot.io) نفسه: سكربت التحليلات الذي يضاف في كل بناء Production، ومسار الخطوط `EXCALIDRAW_ASSET_PATH` الذي يضع الـ CDN أولاً ثم السيرفر نفسه. ولكي لا نعدل الكود نفسه، نضيف طبقة صغيرة فوق الـ Image التي بنيناها، في ملف `/opt/draw/Dockerfile.local`:

```dockerfile
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 فكان المخرج:

```text
      4 https://excalidraw.nyc3.cdn.digitaloceanspaces.com
```

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

⚠️

أمر `sed` في Dockerfile.local يعتمد على نص محدد في index.html، فإذا تغير هذا النص في إصدار قادم فلن يفشل البناء، وإنما سوف يمر الأمر دون أن يغير شيئاً. لذلك بعد كل تحديث أعد تشغيل أمر grep أعلاه على الـ Image الجديدة، ولا تعتمد على أن البناء نجح.

## التشغيل خلف Caddy

نحتاج ثلاث خدمات: الواجهة التي بنيناها، وخادم الغرف [excalidraw-room](https://github.com/excalidraw/excalidraw-room?ref=arabroot.io)، وCaddy كـ Reverse Proxy يوجه المسار `/socket.io/` إلى خادم الغرف وكل ما عداه إلى الواجهة. وخادم الغرف ليس له إلا وسوم بأرقام Commit، وآخرها `sha-03ff435` المبنية في 15 ديسمبر 2023، فنثبت الـ Digest الخاص بها. أنشئ الملف `/opt/draw/compose.yaml`:

```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](https://caddyserver.com/docs/caddyfile/directives/reverse%5Fproxy?ref=arabroot.io) بحسب المسار:

```text
draw.example.com {
	handle /socket.io/* {
		reverse_proxy room:80
	}
	handle {
		reverse_proxy excalidraw:80
	}
}
```

ثم نشغل الخدمات:

```bash
cd /opt/draw
docker compose up -d --build
```

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

- Caddy يصدر شهادة TLS للنطاق تلقائياً عندما يكون النطاق يشير إلى السيرفر والمنفذان 80 و443 مفتوحين، وقد شرحنا ذلك بالتفصيل في [دليل Caddy](https://arabroot.io/articles/%D8%AA%D8%AB%D8%A8%D9%8A%D8%AA-caddy-reverse-proxy/). وفي تجربتنا على جهاز محلي استخدمنا `:80` بدل اسم النطاق، ونشرنا المنفذ على `127.0.0.1` فقط.
- الواجهة وخادم الغرف لا ينشران أي منفذ على السيرفر، فلا يصل إليهما أحد إلا عبر Caddy.
- Caddy يمرر اتصال WebSocket دون إعداد إضافي، وهذا ما يحتاجه Socket.IO.
- الـ Volume الوحيد هو بيانات Caddy التي فيها الشهادات، أما Excalidraw وخادم الغرف فلا يحفظان شيئاً على القرص.

وللتحقق من أن المسارين يعملان، نطلب الصفحة الرئيسية، ثم نطلب Socket.IO بطريقة Polling:

```bash
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`:

```text
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 رابطاً بالشكل التالي:

```text
https://draw.example.com/#room=26dcc716436cbd8d8277,<KEY>
```

والجزء بعد `#room=` فيه قيمتان: رقم الغرفة Room ID، ومفتاح التشفير Key. وأي شيء بعد علامة `#` في الرابط لا يرسله المتصفح إلى السيرفر أصلاً، لذلك يبقى المفتاح في المتصفح، ويرسل المتصفح لخادم الغرف رقم الغرفة فقط ومعه بيانات مشفرة بخوارزمية AES-GCM بمفتاح 128 بت، كما يشرح [مقال الفريق عن التشفير](https://plus.excalidraw.com/blog/end-to-end-encryption?ref=arabroot.io)، وكما وجدنا في كود المشروع.

![رابط الغرفة https://draw.example.com/#room= ثم رقم الغرفة باللون البنفسجي ثم المفتاح باللون الأخضر، والمتصفح A يشفر كل تغيير بـ AES-GCM 128 ويرسله مع رقم الغرفة إلى excalidraw-room الذي يرى رقم الغرفة وبيانات مشفرة فقط، ويعيد بثها إلى المتصفح B الذي يفك التشفير بنفس المفتاح من رابطه](https://arabroot.io/content/images/2026/10/excalidraw-lab-02-e2e.webp)

رقم الغرفة يصل إلى الخادم، والمفتاح لا يغادر المتصفح

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

![نافذة Live collaboration فيها الاسم Harmonious Cod ورابط http://localhost:18704/#room= وزر Copy link، وفوقها رسالة Error تقول Couldn't save to the backend database. If problems persist, you should save your file locally to ensure you don't lose your work، وفي الأعلى علامة تحذير حمراء](https://arabroot.io/content/images/2026/10/excalidraw-lab-03-share-session.webp)

رسالة «Couldn't save to the backend database» متوقعة في بنائنا، لأنه لا يوجد Firebase

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

![لقطتان متجاورتان لنفس اللوحة في متصفحين، في كل منهما مستطيل وسهم وشكل بيضاوي ونصان سيرفر الإنتاج Production Server وقاعدة البيانات Database، وفي كل لقطة مؤشر المستخدم الآخر باسمه: Superb Dolphin في اليسار وGenerous Sandpiper في اليمين](https://arabroot.io/content/images/2026/10/excalidraw-lab-04-two-sessions.webp)

المتصفح B على اليسار والمتصفح A على اليمين، والعناصر الخمسة ظهرت في الاثنين، وكل متصفح يرى مؤشر الآخر واسمه

ولكي نتأكد من أن خادم الغرف لا يرى الرسم، شغلناه مرة بمتغير البيئة `DEBUG='*'` ليطبع كل ما يصله، وهذا جزء من السجل أثناء الرسم:

```text
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 العشوائي الذي يولده المتصفح لكل رسالة.
- لا يظهر في السجل أي نص كتبناه ولا أي إحداثيات، لذلك فمن يصل إلى سجلات السيرفر لا يعرف ماذا رسمنا، ولكنه يعرف متى كانت الغرفة نشطة وكم شخصاً فيها.

🛑

من يملك الرابط كاملاً بما فيه المفتاح يدخل الغرفة ويرى كل شيء، ولا توجد في Excalidraw أي صلاحيات أو تسجيل دخول، لذلك عامل رابط الغرفة معاملة كلمة المرور، ولا تضعه في قناة عامة. وإذا أردت أن تقصر الوصول إلى الأداة على موظفيك فضع أمامها تسجيل دخول في الـ Reverse Proxy أو اجعلها متاحة عبر الـ VPN فقط.

## النص العربي واتجاهه RTL

كتبنا في المتصفح الأول «سيرفر الإنتاج Production Server»، وفي الثاني «قاعدة البيانات Database»، وكما ترى في الصورة السابقة اكتشفت الأداة أن النص يبدأ بالعربية فجعلت اتجاهه من اليمين إلى اليسار، فظهرت الكلمة العربية أولاً للقارئ، وظهرت الحروف متصلة بشكل صحيح. والملاحظة الوحيدة أن العربية تظهر بخط النظام وليس بالخط الذي يشبه اليد Excalifont، لأن هذا الخط لا يحتوي الحروف العربية، فيبدو النص العربي أكثر رسمية من بقية الرسم، وهذا لا يؤثر على القراءة.

## التصدير Export إلى PNG وSVG

من القائمة، أو بالاختصار `Ctrl+Shift+E`، تفتح نافذة تصدير الصورة Export image:

![نافذة Export image فيها معاينة للنص قاعدة البيانات Database، وخيارات Only selected وBackground وDark mode وEmbed scene وScale بقيم 1x و2x و3x، وأزرار PNG وSVG وCopy to clipboard](https://arabroot.io/content/images/2026/10/excalidraw-lab-05-export-dialog.webp)

نافذة التصدير، والخيار Embed scene يضع بيانات الرسم داخل الصورة لتعود وتعدلها

في النافذة أعلاه لاحظ التالي:

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

وبعد إلغاء التحديد صدرنا اللوحة كاملة، فكان ملف PNG بحجم 27 كيلوبايت وأبعاد 699 في 243، وملف SVG بحجم 8 كيلوبايت، وهذه الصورة كما خرجت:

![الصورة المصدرة: مستطيل وسهم وشكل بيضاوي بخط يشبه اليد، وتحتها النصان سيرفر الإنتاج Production Server وقاعدة البيانات Database](https://arabroot.io/content/images/2026/10/excalidraw-lab-06-exported-png.webp)

ملف PNG كما صدرته Excalidraw من نسختنا

وفي ملف SVG وجدنا أن النص العربي محفوظ كنص وليس كأشكال، ومعه اتجاهه:

```text
<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>
```

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

## ماذا يحدث عندما يخرج الجميع؟

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

![لوحة Excalidraw فارغة فيها شاشة الترحيب وعبارة Your drawings are saved in your browser's storage وخيارات Open وHelp وLive collaboration، وعلى زر المشاركة الرقم 1](https://arabroot.io/content/images/2026/10/excalidraw-lab-07-empty-room.webp)

نفس رابط الغرفة بعد خروج الجميع: اللوحة فارغة، ولا يوجد في الغرفة إلا المتصفح الجديد

وهذا هو أثر إزالة Firebase، ففي excalidraw.com تحفظ الغرفة مشفرة في Firebase فتعود عندما يفتح أحدهم الرابط لاحقاً، أما في بنائنا فالغرفة موجودة ما دام فيها شخص واحد على الأقل. والحل البسيط أن يحفظ أحد المشاركين الرسم في نهاية الجلسة من القائمة Save to، أو بالاختصار `Ctrl+S`، كملف `.excalidraw` ويضعه في المستودع بجانب الكود، أو يصدره كـ PNG مع Embed scene. وإذا كنت تحتاج حفظاً دائماً للغرف ومساحة مشتركة للفريق فهذا ما تبيعه [Excalidraw+](https://plus.excalidraw.com/pricing?ref=arabroot.io)، أو تستطيع أن تربط بناءك بمشروع Firebase تملكه أنت، وهذا خارج نطاق هذا الدليل.

⚠️

الصور التي تسحبها إلى اللوحة أثناء الجلسة المشتركة ترفع في النسخة الرسمية إلى Firebase Storage لكي يراها الآخرون، وفي بنائنا لا يوجد مكان ترفع إليه، لذلك استخدم في الجلسات المشتركة الأشكال والنصوص، وأضف الصور بعد الجلسة في ملفك المحفوظ.

## draw.io بجانبه

إذا كان فريقك يحتاج أيضاً مخطط شبكة مفصل بأيقونات السحابة والأجهزة، فـ draw.io مناسب أكثر، وتشغيله أبسط من Excalidraw لأنه لا يحتاج بناء:

```bash
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/
```

```text
200
```

وظهرت الواجهة بالعربية عندما فتحنا `http://localhost:8081/?lang=ar`، ورسمنا فيها نفس المخطط مع ضبط اتجاه النص `textDirection=rtl` لكل شكل:

![واجهة draw.io بقوائم عربية: ملف وتعديل وعرض ورتب وإضافات ومساعدة، وعلى اللوحة المستخدم User وسهم طلب HTTPS إلى API Server الخادم، ومنه SQL إلى Database قاعدة البيانات ورسالة إلى Queue الطابور](https://arabroot.io/content/images/2026/10/diagram-tools-04-drawio-arabic.webp)

draw.io 32.5.5 على سيرفرنا بالواجهة العربية

ولاحظ أمرين قبل أن تعتمد عليه: الأول أن هذه الـ Image تعطل المزامنة في الوقت نفسه، ووجدنا ذلك في ملف الإعداد داخل الـ Container:

```bash
docker exec drawio grep "sync" /usr/local/tomcat/webapps/draw/js/PreConfig.js
```

```text
urlParams['sync'] = 'manual'; //Disable Real-Time
```

والثاني أن [ملف README في المستودع](https://github.com/jgraph/drawio?ref=arabroot.io) يقول إن هذه النسخة للتجربة والتقييم، وإن هناك نسخة إنتاج مدفوعة، لذلك استخدم draw.io المستضاف للرسم الفردي، واحفظ الملفات بصيغة `.drawio.svg` في المستودع، واجعل Excalidraw للجلسات المشتركة. ولكي تضعه خلف نفس الـ Reverse Proxy أضف له نطاقاً فرعياً مثل `diagrams.example.com` بنفس طريقة Caddyfile أعلاه.

## مخطط Mermaid من سطر الأوامر CLI

وأخيراً، المخططات التي تعيش داخل المستودع ككود لا تحتاج سيرفراً أصلاً، فـ GitHub وGitLab يعرضان Mermaid مباشرة، ولكن إذا أردت صورة لتضعها في عرض تقديمي أو في Wiki لا يعرض Mermaid، فأداة [mermaid-cli](https://github.com/mermaid-js/mermaid-cli?ref=arabroot.io) تحولها. وهذا ملف `flow-rtl.mmd` بتسميات عربية مع `dir='rtl'` الذي وجدناه ضرورياً في [اختبار العربية](https://arabroot.io/articles/%D8%A7%D8%AF%D9%88%D8%A7%D8%AA-%D8%B1%D8%B3%D9%85-%D8%A7%D9%84%D9%85%D8%AE%D8%B7%D8%B7%D8%A7%D8%AA-diagrams-%D9%85%D9%82%D8%A7%D8%B1%D9%86%D8%A9-%D8%B9%D9%85%D9%84%D9%8A%D8%A9/#arabic):

```text
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>")]
```

```bash
docker run --rm -v "$PWD":/data minlag/mermaid-cli:12.0.1 -i flow-rtl.mmd -o flow-rtl.png -s 2 -b white
```

```text
Generating single mermaid chart
```

![مخطط تدفق من اليسار إلى اليمين: User المستخدم ثم سهم HTTPS طلب إلى API Server الخادم ثم سهم SQL إلى أسطوانة قاعدة البيانات Database](https://arabroot.io/content/images/2026/10/excalidraw-lab-09-mermaid-cli.webp)

المخرج من mermaid-cli، والكلمة العربية في كل تسمية هي الأولى من اليمين

ولاحظ أن الـ 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` التي يحفظها الفريق، ومكانها المستودعات نفسها، فإذا ضاع السيرفر بقيت المخططات.

```bash
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` على الملف الجديد:

```bash
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 خادم الغرف، ثم فتحنا رابط غرفة جديدة، فلم تظهر أي رسالة خطأ، وفتحت اللوحة كأنها لوحة عادية، وكل ما تغير أن زر المشاركة ليس عليه رقم:

![لوحة Excalidraw بشاشة الترحيب، وزر المشاركة الأخضر في الأعلى بدون أي رقم عليه، ولا توجد رسالة خطأ](https://arabroot.io/content/images/2026/10/excalidraw-lab-10-no-room.webp)

عندما يتوقف خادم الغرف تعمل اللوحة محلياً بصمت، وزر المشاركة بدون رقم

وهذه أخطر حالة، لأن كل شخص يظن أنه في جلسة مشتركة وهو يرسم وحده، لذلك إذا قال أحد الزملاء إنه لا يرى ما ترسمه فانظر أولاً إلى الرقم على زر المشاركة، ثم شغل أمر `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، أما خطوط الرسم نفسها فتحمل من السيرفر، والرسم والتصدير لا يتأثران.

## إزالة التجربة

إذا كنت جربت على جهازك وتريد إزالة كل شيء:

```bash
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 يحول مخططات الكود إلى صور.

وأقرب المقالات إلى هذا الموضوع على الموقع:

[![](https://arabroot.io/content/images/2026/10/diagram-tools-cover-cover.webp)دليلأدوات رسم المخططات Diagrams: مقارنة عمليةمقارنة أدوات الرسم مفتوحة المصدر والتجارية بالترخيص والاستضافة والرسم المشترك وGit، مع اختبار فعلي للعربية.](https://arabroot.io/articles/%D8%A7%D8%AF%D9%88%D8%A7%D8%AA-%D8%B1%D8%B3%D9%85-%D8%A7%D9%84%D9%85%D8%AE%D8%B7%D8%B7%D8%A7%D8%AA-diagrams-%D9%85%D9%82%D8%A7%D8%B1%D9%86%D8%A9-%D8%B9%D9%85%D9%84%D9%8A%D8%A9/)[![](https://arabroot.io/content/images/2026/10/diagrams-concept-cover-cover.webp)دليلالمخططات التقنية Diagrams: ماذا ترسم ومتى؟كيف تختار نوع المخطط من سؤال القارئ، ومتى ترسم باليد أو بالكود، وكيف تبقيه صحيحاً ومقروءاً بالعربية.](https://arabroot.io/articles/%D8%A7%D9%84%D9%85%D8%AE%D8%B7%D8%B7%D8%A7%D8%AA-%D8%A7%D9%84%D8%AA%D9%82%D9%86%D9%8A%D8%A9-diagrams-%D9%85%D8%A7%D8%B0%D8%A7-%D8%AA%D8%B1%D8%B3%D9%85-%D9%88%D9%85%D8%AA%D9%89/)[![](https://arabroot.io/content/images/2026/10/caddy-cover-cover.webp)دليلتثبيت Caddy على خادمك: Reverse Proxy بشهادات HTTPS تلقائيةكيف تثبت Caddy على سيرفرك ليمنح كل موقع شهادة HTTPS تلقائياً، وتربط به تطبيقاتك بملف إعداد من بضعة أسطر.](https://arabroot.io/articles/%D8%AA%D8%AB%D8%A8%D9%8A%D8%AA-caddy-reverse-proxy/)[![](https://arabroot.io/content/images/2026/10/outline-cover-cover.webp)دليلOutline: قاعدة معرفة فريقك على سيرفرككيف تشغل Outline على سيرفرك ليكتب فيه فريقك إجراءاته وقراراته، ويدخل إليه الموظفون بحساباتهم في authentik.](https://arabroot.io/articles/%D8%AA%D8%AB%D8%A8%D9%8A%D8%AA-outline-%D9%84%D9%82%D8%A7%D8%B9%D8%AF%D8%A9-%D9%85%D8%B9%D8%B1%D9%81%D8%A9-%D8%A7%D9%84%D9%81%D8%B1%D9%8A%D9%82/)

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

- أكتوبر 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.