> ## 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.

# Canvas LMS المفتوح المصدر Open Source على سيرفرك
- URL: https://arabroot.io/articles/تثبيت-canvas-lms-بنسخته-المفتوحة/
- Published: 2026-10-14T08:15:00.000Z
- Updated: 2026-10-25T08:50:02.000Z
- Description: ما الذي تحصل عليه من Canvas المفتوح وما الذي يبقى في نسخة Instructure المستضافة، وكيف تبنيه وتشغله وتنشئ أول مقرر.
- Author: فريق عرب رووت
- Tags: الأدلة التقنية, التعليم والبحث العلمي, تطبيقات لفريقك, الاستضافة الذاتية, Canvas LMS

كثير من الطلاب الذين درسوا في جامعات خارج المنطقة يعرفون Canvas من نسخته المستضافة، فيأتي السؤال إلى قسم تقنية المعلومات: الكود مفتوح المصدر على GitHub، فلماذا لا نشغله على سيرفراتنا دون رسوم؟ والإجابة المختصرة أن ذلك ممكن، ولكن ما تحصل عليه ليس هو نفسه ما يراه الطالب في النسخة المستضافة، والتثبيت أثقل بكثير من [Moodle](https://arabroot.io/articles/%D8%AA%D8%AB%D8%A8%D9%8A%D8%AA-moodle-%D9%85%D9%86%D8%B5%D8%A9-%D8%A7%D9%84%D8%AA%D8%B9%D9%84%D9%85-%D8%B9%D9%84%D9%89-%D8%B3%D9%8A%D8%B1%D9%81%D8%B1%D9%83/) و[Chamilo](https://arabroot.io/articles/%D8%AA%D8%AB%D8%A8%D9%8A%D8%AA-chamilo-%D9%85%D9%86%D8%B5%D8%A9-%D8%AA%D8%B9%D9%84%D9%85-%D8%AE%D9%81%D9%8A%D9%81%D8%A9-%D9%88%D8%B3%D9%87%D9%84%D8%A9/). وفي مقال [أنظمة إدارة التعلم LMS](https://arabroot.io/articles/%D8%A7%D9%86%D8%B8%D9%85%D8%A9-%D8%A7%D8%AF%D8%A7%D8%B1%D8%A9-%D8%A7%D9%84%D8%AA%D8%B9%D9%84%D9%85-lms-%D9%85%D8%A7%D8%B0%D8%A7-%D8%AA%D8%AD%D8%AA%D8%A7%D8%AC-%D9%85%D9%86%D9%87%D8%A7-%D9%85%D8%A4%D8%B3%D8%B3%D8%AA%D9%83/#canvas) ذكرنا أنه يناسب جامعة لديها فريق يعرف Rails، وفي هذا المقال سوف نتناول:

- ما الذي في المستودع المفتوح، وما الذي تبقيه Instructure لنسختها المستضافة.
- حالة المستودع العام اليوم، ولماذا يجب أن تنتبه لتاريخ آخر تحديث فيه.
- بناء Canvas من فرع `prod` بالملف `Dockerfile.production` الذي في المستودع، مع عطلين في البناء وعطلين في التشغيل.
- مقرر ومعلمة وطلاب وواجب ودرجة عبر واجهة Canvas البرمجية REST API، والعربية، والموارد، والنسخ الاحتياطي.

## ما الذي تحصل عليه من النسخة المفتوحة Open Source؟

[Canvas LMS](https://github.com/instructure/canvas-lms?ref=arabroot.io) تطبيق Ruby on Rails منشور بترخيص AGPLv3، وصفحة [الأسئلة الشائعة FAQ](https://github.com/instructure/canvas-lms/wiki/FAQ?ref=arabroot.io) في المستودع تشرح نموذج الترخيص بوضوح: Canvas مرخص ترخيصاً مزدوجاً Dual Licensing، فهو AGPLv3 للمجتمع، واشتراك تجاري لعملاء Instructure، ولهذا تطلب الشركة اتفاقية مساهمة CLA من كل من يرسل كوداً. وتقول الصفحة نفسها إن النسخة المستضافة تعمل بالكود نفسه الذي في GitHub، ولكن معها إضافات لا تنشرها الشركة، والصورة التالية تبين القسمين:

![مخطط ما في Canvas المفتوح وما يبقى عند Instructure: على اليمين على سيرفرك من المستودع العام AGPLv3 سيرفر التطبيق فيه تطبيق الويب Rails مع nginx وPassenger والمهام المؤجلة delayed_job وقاعدة البيانات PostgreSQL 14 والذاكرة المؤقتة Redis، والميزات المقررات والوحدات والواجبات وسجل الدرجات والاختبارات الكلاسيكية والنقاشات وأدوات LTI وREST API والتقويم والإعلانات والدخول عبر SAML، والدعم من المجتمع فقط. وعلى اليسار في Canvas المستضاف فقط: New Quizzes وMasteryPaths وCatalog وCanvas Data وDocViewer وNew Analytics وتكامل SIS التجاري ومنصة كشف الانتحال وأداة استيراد SCORM](https://arabroot.io/content/images/2026/10/canvas-lms-01-open-vs-hosted.webp)

الكود الأساسي واحد، والفرق في الإضافات المغلقة والخدمات التي تديرها الشركة

والقائمة التي في الصورة هي كما في صفحة الأسئلة الشائعة: New Quizzes، وMasteryPaths، ومنصة كشف الانتحال Plagiarism Platform، وتكاملات SIS التجارية، وأدوات الترحيل من الأنظمة التجارية، وأداة المحادثة Chat، وأداة استيراد SCORM، وCatalog، وCanvas Data، وDocViewer، وNew Analytics، وتوليد رمز الدخول QR. ولاحظ أن آخر تعديل لهذه الصفحة كان في 19 مايو 2020، فهي القائمة الرسمية المنشورة، ولكنها قديمة، وقد تكون الشركة أضافت منذ ذلك الوقت خدمات أخرى مستضافة فقط، لذلك اسأل Instructure عن أي ميزة بعينها قبل أن تعد بها المعلمين. وأهم ما يلاحظه المعلم من هذه القائمة غياب New Quizzes، فالاختبارات في النسخة المفتوحة هي الاختبارات الكلاسيكية Classic Quizzes.

### حالة المستودع العام اليوم

قبل أن تبني أي شيء، انظر إلى تاريخ آخر تحديث، فعند كتابة هذا المقال في أكتوبر 2026 كان آخر Commit في فرع `master` وفي فرع `prod` بتاريخ 30 أبريل 2026، و[سجل فرع prod](https://github.com/instructure/canvas-lms/commits/prod?ref=arabroot.io) يقول إن آخر نسخة فيه مطابقة لفرع `stable/2026-04-22`، أي أن المستودع العام لم يتحدث منذ ما يقارب ستة أشهر، ولم نجد إعلاناً رسمياً يشرح السبب. وهذا مهم لأمرين: الأول أن الإصلاحات الأمنية التي تصدر للنسخة المستضافة بعد ذلك التاريخ لا تصل إليك، والثاني أننا سوف نرى بعد قليل أن هذا الفرع نفسه لا يبنى كما هو. لذلك تابع المستودع قبل أن تقرر، ولا تبن خطة جامعة كاملة على كود لا تعرف هل سوف يتحدث.

## المتطلبات Requirements

[دليل التشغيل في الإنتاج Production Start](https://github.com/instructure/canvas-lms/wiki/Production-Start?ref=arabroot.io) هو المرجع الرسمي، وأهم ما فيه:

| البند          | كما في الدليل                                                                                                                                  |
| -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| النظام         | كتب واختبر على Ubuntu 22.04 LTS                                                                                                                |
| الذاكرة RAM    | 8GB على الأقل، خاصة إذا كان كل شيء على سيرفر واحد                                                                                              |
| قاعدة البيانات | PostgreSQL 14 أو أحدث منذ إصدار stable/2024-11-20                                                                                              |
| اللغة          | Ruby 3.4.1 أو أحدث، وNode.js 20، وYarn 1.19.1                                                                                                  |
| الخدمات        | Redis، وبريد صادر SMTP، وعملية مستقلة للمهام المؤجلة Jobs                                                                                      |
| محرر المحتوى   | خدمة [canvas-rce-api](https://github.com/instructure/canvas-rce-api?ref=arabroot.io) مطلوبة لكامل ميزات محرر المحتوى الغني Rich Content Editor |

ويفترض الدليل أنك تعرف إعداد Apache وتطبيقات Rails وPassenger وPostgreSQL، ويحيلك في الدعم إلى قائمة المراسلات المجتمعية، وأما [دليل البداية السريعة Quick Start](https://github.com/instructure/canvas-lms/wiki/Quick-Start?ref=arabroot.io) فيقول صراحة إنه لبيئة تطوير وليس للإنتاج، وإن سكربت Docker الذي فيه ينصح بمساحة 150GB على القرص.

## بناء Canvas من المستودع

الطريقة البسيطة هي البحث عن Docker Image جاهز على Docker Hub، ولكن آخر تحديث لـ `instructure/canvas-lms` هناك كان في يونيو 2019، فهو غير صالح إطلاقاً. والطريقة الثانية هي اتباع دليل الإنتاج خطوة بخطوة على Ubuntu، وهي الطريقة الرسمية. ولكن المستودع نفسه فيه ملف اسمه [Dockerfile.production](https://github.com/instructure/canvas-lms/blob/prod/Dockerfile.production?ref=arabroot.io) ينفذ الخطوات نفسها: يثبت Node وPostgreSQL Client فوق Docker Image من Instructure فيه Ruby 3.4 وnginx وPassenger، ثم `bundle install`، ثم `yarn install`، ثم `canvas:compile_assets`، لذلك سوف نبني منه.

ولأن المستودع كبير، نترك Docker يجلبه بنفسه من GitHub بدلاً من نسخه ثم إرساله كسياق بناء Build Context، فقد أخذ إرسال السياق من نسخة محلية أكثر من ربع ساعة في تجربتنا الأولى:

```bash
docker build --progress=plain -f Dockerfile.production -t canvas:prod-2026-04-22 \
  "https://github.com/instructure/canvas-lms.git#prod"
```

### نجرب العطل: npm cache clean

توقف البناء في الخطوة الخامسة من عشر خطوات بالخطأ التالي، وتكرر في المحاولة الثانية:

```text
#7 [ 5/10] RUN npm install -g npm@9.8.1 && npm cache clean --force
#7 210.6 npm ERR! code ENOTEMPTY
#7 210.6 npm ERR! syscall rmdir
#7 210.6 npm ERR! path /root/.npm/_cacache
#7 210.6 npm ERR! ENOTEMPTY: directory not empty, rmdir '/root/.npm/_cacache'
```

والسطر الذي فشل هو تنظيف ذاكرة npm المؤقتة فقط، وليس التثبيت، فقد طبع npm قبله «removed 17 packages, and changed 91 packages in 3m»، لذلك استبدلناه بحذف المجلد مباشرة، وهو يؤدي الغرض نفسه:

```dockerfile
RUN npm install -g npm@9.8.1 && rm -rf /root/.npm
```

### نجرب العطل: حزمة غير معلنة في الكود

بعد أكثر من ساعة توقف البناء مرة ثانية في بناء الواجهة الأمامية بأداة Rspack:

```text
#13 3300.3 ERROR in ./ui/features/discovery_page/react/components/ConfigureModal.tsx 28:0-62
#13 3300.3   × Module not found: Can't resolve '@instructure/platform-alerts' in '/usr/src/app/ui/features/discovery_page/react/components'
#13 3300.3 Rspack compiled with 1 error in 59.36 s
#13 3352.1 Parallel::UndumpableException: RuntimeError: Error running js:webpack_development:  (Parallel::UndumpableException)
```

وعند البحث في الكود وجدنا أن ملفاً واحداً يستورد الحزمة `@instructure/platform-alerts`، بينما الملف `package.json` في فرع `prod` يعلن عشر حزم من عائلة `@instructure/platform-*` ليس منها هذه الحزمة، ولا توجد في `yarn.lock`، مع أنها منشورة في سجل npm العام وأول إصداراتها في أبريل 2026\. أي أن الفرع الذي يفترض أنه جاهز للإنتاج لا يبنى كما هو، وهذا مثال عملي على ما ذكرناه عن حالة المستودع. والحل الذي استخدمناه هو إضافة الحزمة قبل بناء الواجهة، بالإصدار 1.0.0 الذي صدر قبل آخر Commit في الفرع:

```dockerfile
RUN (yarn install --frozen-lockfile || yarn install --frozen-lockfile --network-concurrency 1) && yarn add --ignore-workspace-root-check @instructure/platform-alerts@1.0.0 && \
  bin/rails canvas:compile_assets --trace && \
  rm -rf node_modules
```

وبعد التعديلين نجح البناء، وهذه أزمنة مراحل `compile_assets` كما طبعها، وكانت الخطوة العاشرة كاملة مع `yarn install` قد أخذت 1318 ثانية:

```text
--> Finished: 'doc:api' in 85.43s
--> Finished: 'i18n:generate_js' in 136.66s
--> Finished: 'js:webpack_development' in 207.48s
--> Finished: 'js:webpack_production' in 244.1s
--> Finished: 'css:compile' in 250.47s
```

وحجم ال Docker Image الناتج 10.7GB، وهو رقم يجب أن تحسبه في مساحة السيرفر وفي سجل ال Images إذا كنت تبني لكل تحديث.

## ملفات الإعداد

يقرأ Canvas إعداداته من ملفات YAML في `config/`، وسوف نضعها خارج ال Image ونربطها للقراءة فقط، والأسرار تأتي من متغيرات البيئة بصيغة ERB التي يدعمها Rails. أنشئ أولاً ملف `.env` بقيم عشوائية:

```bash
cat > .env <<EOF
CANVAS_DB_PASSWORD=$(openssl rand -hex 16)
CANVAS_ENCRYPTION_KEY=$(openssl rand -hex 32)
CANVAS_JWT_KEY=$(openssl rand -hex 32)
EOF
```

ثم ملفات `config` التالية، وكل واحد منها مختصر من الملف `.example` المقابل له في المستودع:

```yaml
# config/database.yml
production:
  adapter: postgresql
  host: db
  database: canvas_production
  username: canvas
  password: <%= ENV.fetch('CANVAS_DB_PASSWORD') %>
  encoding: utf8
  timeout: 5000
  prepared_statements: false

# config/domain.yml
production:
  domain: "canvas.localhost:27443"
  ssl: true

# config/security.yml
production:
  encryption_key: <%= ENV.fetch('CANVAS_ENCRYPTION_KEY') %>
  jwt_encryption_keys:
    - <%= ENV.fetch('CANVAS_JWT_KEY') %>
  lti_iss: 'https://canvas.example.com'

# config/redis.yml
production:
  url: redis://redis

# config/cache_store.yml
production:
  cache_store: redis_cache_store

# config/delayed_jobs.yml
production:
  workers:
  - queue: canvas_queue
    workers: 2

# config/file_store.yml
production:
  storage: local
  path_prefix: tmp/files

# config/outgoing_mail.yml
production:
  address: "smtp.example.com"
  port: "587"
  user_name: "canvas@example.com"
  password: "change-me-smtp"
  authentication: "plain"
  domain: "example.com"
  outgoing_address: "canvas@example.com"
  default_name: "Lab Canvas"

# config/dynamic_settings.yml
production:
  config:
    canvas: {}
```

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

- **مفتاح التشفير encryption\_key:** يشفر به Canvas بيانات حساسة في قاعدة البيانات، والدليل يطلب نصاً عشوائياً من 20 حرفاً على الأقل، وإذا فقدته فقدت القدرة على قراءة تلك البيانات، لذلك هو جزء من النسخة الاحتياطية.
- **domain.yml:** النطاق الذي يبني منه Canvas الروابط في الرسائل والمهام الخلفية، وفي المختبر استخدمنا `canvas.localhost` لأن المتصفحات تحوله إلى الجهاز نفسه، وفي الإنتاج يكون نطاقك.
- **file\_store.yml:** الملفات المرفوعة في مجلد محلي، والبديل S3 عبر `amazon_s3.yml`، وهو الأنسب إذا كان لديك أكثر من سيرفر.

## التشغيل بـ Docker Compose

```yaml
name: edu1-canvas
x-canvas: &canvas
  image: edu1-canvas:prod-2026-04-22
  restart: unless-stopped
  depends_on:
    db:
      condition: service_healthy
    redis:
      condition: service_started
  environment:
    RAILS_ENV: production
    CANVAS_DB_PASSWORD: ${CANVAS_DB_PASSWORD}
    CANVAS_ENCRYPTION_KEY: ${CANVAS_ENCRYPTION_KEY}
    CANVAS_JWT_KEY: ${CANVAS_JWT_KEY}
    PASSENGER_STARTUP_TIMEOUT: "300"
    PASSENGER_MAX_POOL_SIZE: "2"
  volumes:
    - ./config/database.yml:/usr/src/app/config/database.yml:ro
    - ./config/domain.yml:/usr/src/app/config/domain.yml:ro
    - ./config/security.yml:/usr/src/app/config/security.yml:ro
    - ./config/redis.yml:/usr/src/app/config/redis.yml:ro
    - ./config/cache_store.yml:/usr/src/app/config/cache_store.yml:ro
    - ./config/delayed_jobs.yml:/usr/src/app/config/delayed_jobs.yml:ro
    - ./config/file_store.yml:/usr/src/app/config/file_store.yml:ro
    - ./config/outgoing_mail.yml:/usr/src/app/config/outgoing_mail.yml:ro
    - ./config/dynamic_settings.yml:/usr/src/app/config/dynamic_settings.yml:ro
    - files:/usr/src/app/tmp/files

services:
  db:
    image: pgvector/pgvector:0.8.7-pg14
    container_name: edu1-canvas-db
    restart: unless-stopped
    environment:
      POSTGRES_DB: canvas_production
      POSTGRES_USER: canvas
      POSTGRES_PASSWORD: ${CANVAS_DB_PASSWORD}
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U canvas -d canvas_production"]
      interval: 10s
      retries: 10
    mem_limit: 1g
    volumes:
      - db:/var/lib/postgresql/data

  redis:
    image: redis:7.4.10
    container_name: edu1-canvas-redis
    restart: unless-stopped
    mem_limit: 256m

  web:
    <<: *canvas
    container_name: edu1-canvas-web
    mem_limit: 4g

  caddy:
    image: caddy:2.11.7-alpine
    container_name: edu1-canvas-caddy
    restart: unless-stopped
    depends_on: [web]
    ports:
      - "127.0.0.1:27443:443"
    mem_limit: 128m
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy:/data

  jobs:
    <<: *canvas
    container_name: edu1-canvas-jobs
    command: bundle exec script/delayed_job run
    mem_limit: 2g

volumes:
  db:
    name: edu1-canvas-db
  files:
    name: edu1-canvas-files
  caddy:
    name: edu1-canvas-caddy
```

والملف `Caddyfile`:

```text
canvas.localhost:443 {
	tls internal
	reverse_proxy web:80
}
```

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

- **pgvector:** استخدمنا PostgreSQL 14 مع إضافة pgvector، لأن ملف PostgreSQL في `docker-compose` الذي في المستودع يثبتها، وبعد التهيئة وجدنا في قاعدة البيانات الإضافات `pg_trgm` و`vector`.
- **Container مستقل للمهام Jobs:** بالأمر `script/delayed_job run`، والدليل ينصح بفصل خادم المهام عن خادم الويب عندما تكبر الحركة، لأن المهام الخلفية مثل الإشعارات والتقارير تأخذ من موارد الطلاب.
- **Caddy أمام Canvas:** سوف نرى في العطل الرابع لماذا لا يعمل Canvas في الإنتاج دون HTTPS، و`tls internal` يصدر شهادة من جهة إصدار داخلية للمختبر فقط.

### تهيئة قاعدة البيانات

الأمر `db:initial_setup` ينشئ الجداول وحساب المدير، والدليل يذكر متغيرات البيئة التي تغني عن الأسئلة التفاعلية:

```bash
docker compose up -d db redis
docker compose run --rm \
  -e CANVAS_LMS_ADMIN_EMAIL=admin@example.com \
  -e CANVAS_LMS_ADMIN_PASSWORD='Lab-Admin-2026!' \
  -e CANVAS_LMS_ACCOUNT_NAME='Lab University' \
  -e CANVAS_LMS_STATS_COLLECTION=opt_out \
  web bundle exec rake db:initial_setup
```

```text
==  InitCanvasDb: migrating ===================================================
-- create_extension(:pg_trgm, {schema: "public", if_not_exists: true})
   -> 0.0307s
...
Notifications Loaded
...
Initial data loaded
```

واستغرقت التهيئة 114 ثانية، وأنشأت 327 جدولاً. ولأن الواجهة بنيت دون قاعدة بيانات، يطلب الدليل بعدها توليد ملفات القوالب Brand Configs:

```bash
docker compose run --rm web bundle exec rake brand_configs:generate_and_upload_all
docker compose up -d web jobs caddy
```

### نجرب العطل: Passenger لا يصبر على Rails

في أول تشغيل أرجع كل طلب الخطأ 500 بعد 90 ثانية، وفي سجل ال Container:

```text
[ E 2026-10-22 04:31:47.7321 ... ]: Could not spawn process for application /usr/src/app: A timeout occurred while starting a preloader process.
192.168.80.1 - - [22/Oct/2026:04:31:47 +0000] "GET / HTTP/1.1" 500 4393 "-" "curl/8.12.1"
```

والسبب أن تحميل Rails مع كل مكتبات Canvas أخذ أكثر من 90 ثانية على سيرفر مشغول، وهي القيمة الافتراضية لـ [passenger\_start\_timeout](https://www.phusionpassenger.com/docs/references/config%5Freference/nginx/?ref=arabroot.io#passenger%5Fstart%5Ftimeout)، وال Docker Image من Instructure يقرأ هذه القيمة من المتغير `PASSENGER_STARTUP_TIMEOUT` عند توليد ملف nginx، لذلك رفعناها إلى 300 كما في ملف Compose، وخفضنا `PASSENGER_MAX_POOL_SIZE` من 6 إلى 2 لتوفير الذاكرة. وبعد ذلك أصبح أول طلب يأخذ نحو 3 دقائق، وما بعده أقل من ثانية:

```text
code=200 after 184s
200 0.147354
```

### نجرب العطل: الدخول عبر HTTP

قبل أن نضيف Caddy، كان خادم الويب منشوراً مباشرة على HTTP، فظهرت صفحة الدخول بالعربية، ولكن عند إدخال البريد وكلمة المرور ظهرت الرسالة «رمز مصادقة غير صالح»، أي Invalid Authenticity Token. والسبب في [config/environments/production.rb](https://github.com/instructure/canvas-lms/blob/prod/config/environments/production.rb?ref=arabroot.io)، ففيه `config.force_ssl = true`، وعندها يجعل Canvas ملفات تعريف الارتباط Cookies آمنة Secure، والمتصفح لا يحفظها على اتصال HTTP، فيضيع رمز الحماية من هجمات CSRF بين الصفحة والطلب. والقاعدة: Canvas في وضع الإنتاج لا يعمل إلا خلف HTTPS، حتى في المختبر. وبعد إضافة Caddy ظهرت الترويسات كما ينبغي:

```text
Set-Cookie: log_session_id=23e4fc6c66102ddfeefaecaeecb6af34; path=/; secure; httponly
```

والصورة التالية لصفحة الدخول بالعربية:

![صفحة تسجيل الدخول في Canvas بالعربية على خلفية داكنة: شعار CANVAS، وحقلا البريد الإلكتروني وكلمة المرور، وزر تسجيل الدخول، وخيار الإبقاء على تسجيل الدخول، ورابط هل نسيت كلمة المرور، وفي الأسفل نظام LMS مفتوح المصدر وتعليمات وشعار INSTRUCTURE](https://arabroot.io/content/images/2026/10/canvas-lms-02-login-ar.webp)

صفحة الدخول بالعربية، وهي تعمل فقط عبر HTTPS في وضع الإنتاج

## المقرر والمستخدمون عبر REST API

من أقوى ما في Canvas [واجهته البرمجية REST API](https://canvas.instructure.com/doc/api/?ref=arabroot.io)، فكل ما تفعله في الواجهة تقريباً تفعله بطلب HTTP، وهذا ما تحتاجه الجامعة لربطه بنظام معلومات الطلاب كما شرحنا في [الأنظمة التي تحتاجها الجامعة](https://arabroot.io/articles/%D8%A7%D9%84%D8%A7%D9%86%D8%B8%D9%85%D8%A9-%D8%A7%D9%84%D8%AA%D9%8A-%D8%AA%D8%AD%D8%AA%D8%A7%D8%AC%D9%87%D8%A7-%D8%A7%D9%84%D8%AC%D8%A7%D9%85%D8%B9%D8%A9-%D9%85%D9%86-%D8%A7%D9%84%D9%82%D8%A8%D9%88%D9%84-%D8%A7%D9%84%D9%89-%D8%A7%D9%84%D8%AA%D8%AE%D8%B1%D8%AC/). والخطوة الأولى رمز وصول Access Token للمدير، وأنشأناه من سطر الأوامر، وفي الإنتاج تنشئه من صفحة الإعدادات في حساب المدير:

```bash
docker exec edu1-canvas-web bash -lc 'cd /usr/src/app && bundle exec rails runner "puts User.find(1).access_tokens.create!(purpose: %q(lab)).full_token"'
```

ثم أنشأنا المقرر والمستخدمين والتسجيل بسكربت Python صغير، والطلبات بالترتيب كما في [توثيق المقررات](https://canvas.instructure.com/doc/api/courses.html?ref=arabroot.io):

```python
import json, urllib.request, urllib.parse
T = open('token.txt').read().strip()
B = 'https://canvas.example.com/api/v1'

def call(method, path, data=None, as_user=None):
    if as_user:
        path += ('&' if '?' in path else '?') + 'as_user_id=' + str(as_user)
    body = urllib.parse.urlencode(data, doseq=True).encode() if data else None
    r = urllib.request.Request(B + path, data=body, method=method,
                               headers={'Authorization': 'Bearer ' + T})
    with urllib.request.urlopen(r) as resp:
        return json.loads(resp.read().decode('utf-8'))

c = call('POST', '/accounts/1/courses', {'course[name]': 'مقدمة في الشبكات',
         'course[course_code]': 'NET101', 'course[locale]': 'ar', 'offer': 'true'})
print(c['id'], c['course_code'], c['workflow_state'], c['locale'])

for login, name, pw in [('teacher1', 'سارة المعلمة', 'Lab-Teach-2026!'),
                        ('student1', 'خالد الطالب', 'Lab-Stud-2026!'),
                        ('student2', 'ريم الطالبة', 'Lab-Stud-2026!')]:
    u = call('POST', '/accounts/1/users', {'user[name]': name, 'user[locale]': 'ar',
             'user[skip_registration]': 'true', 'pseudonym[unique_id]': login + '@example.com',
             'pseudonym[password]': pw, 'pseudonym[send_confirmation]': 'false'})
    print(u['id'], u['name'], u['login_id'])

for uid, t in [(2, 'TeacherEnrollment'), (3, 'StudentEnrollment'), (4, 'StudentEnrollment')]:
    e = call('POST', '/courses/1/enrollments', {'enrollment[user_id]': uid, 'enrollment[type]': t,
             'enrollment[enrollment_state]': 'active', 'enrollment[notify]': 'false'})
    print(e['id'], e['type'], e['enrollment_state'])
```

```text
1 NET101 available ar
2 سارة المعلمة teacher1@example.com
3 خالد الطالب student1@example.com
4 ريم الطالبة student2@example.com
1 TeacherEnrollment active
2 StudentEnrollment active
3 StudentEnrollment active
```

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

```python
m = call('POST', '/courses/1/modules', {'module[name]': 'الأسبوع الأول: أساسيات الشبكات'})
a = call('POST', '/courses/1/assignments', {'assignment[name]': 'واجب: شرح سجلات DNS',
         'assignment[points_possible]': 10, 'assignment[submission_types][]': 'online_text_entry',
         'assignment[published]': 'true',
         'assignment[description]': '<p>اشرح الفرق بين السجل A والسجل CNAME بمثال من نطاق example.com.</p>'})
call('POST', f'/courses/1/modules/{m["id"]}/items', {'module_item[type]': 'Assignment',
     'module_item[content_id]': a['id']})
call('PUT', f'/courses/1/modules/{m["id"]}', {'module[published]': 'true'})
```

والصورة التالية للمقرر كما تراه المعلمة، بالوحدة والواجب، والقائمة الجانبية بالعربية:

![صفحة الوحدات في مقرر NET101 على Canvas بالعربية من اليمين إلى اليسار: الوحدة الأسبوع الأول أساسيات الشبكات منشورة، وفيها واجب شرح سجلات DNS بعشر نقاط، وقائمة المقرر الصفحة الرئيسية والإعلانات والمهام والمناقشات والدرجات والأشخاص والصفحات والملفات والمناهج والنتائج ومعايير التقييم والاختبارات والوحدات وإعدادات، والقائمة العامة الداكنة فيها الحساب ولوحة المعلومات والمساقات والتقويم وعلبة الوارد وتعليمات](https://arabroot.io/content/images/2026/10/canvas-lms-03-modules.webp)

الوحدات Modules هي طريقة Canvas في ترتيب المقرر أسبوعاً بأسبوع

ثم سلم الطالب خالد الواجب، وصححته المعلمة بثماني درجات مع تعليق، وكلاهما عبر الواجهة البرمجية كما في [توثيق التسليمات](https://canvas.instructure.com/doc/api/submissions.html?ref=arabroot.io)، والخيار `as_user_id` يجعل المدير ينفذ الطلب نيابة عن الطالب، وهو مفيد في الاختبار وخطير في الإنتاج، فلا تعط رمز المدير لأي سكربت لا تحتاجه:

```python
s = call('POST', '/courses/1/assignments/1/submissions', {'submission[submission_type]': 'online_text_entry',
         'submission[body]': '<p>السجل A يربط الاسم بعنوان IP مباشرة ...</p>'}, as_user=3)
g = call('PUT', '/courses/1/assignments/1/submissions/3', {'submission[posted_grade]': '8',
         'comment[text_comment]': 'شرح صحيح، أضف مثالاً على CNAME.'})
print([(x['user_id'], x['score'], x['workflow_state']) for x in call('GET', '/courses/1/assignments/1/submissions')])
```

```text
[(3, 8.0, 'graded'), (4, None, 'unsubmitted')]
```

والصورتان التاليتان لسجل الدرجات Gradebook كما تراه المعلمة، ثم لصفحة الدرجات كما يراها الطالب:

![دفتر الدرجات في Canvas بالعربية لمقرر NET101: بحث عن الطلاب وبحث عن المهام، وصف خالد الطالب بدرجة 8 في الواجب و80% في الإجمالي، وصف ريم الطالبة بشرطات لأنها لم تسلم](https://arabroot.io/content/images/2026/10/canvas-lms-05-gradebook.webp)

سجل الدرجات: 8 لخالد أي 80%، وشرطة لريم التي لم تسلم

![صفحة الدرجات الخاصة بخالد الطالب في Canvas بالعربية: الواجب شرح سجلات DNS تم الإرسال 22 أكتوبر في 7:43 والدرجة معروضة 10 / 8، ومجموعة Assignments بالإنجليزية 80% و10.00 / 8.00، والإجمالي 80%، وفي الجانب الإجمالي 80% وخيار يعتمد الحساب فقط على المهام التي تم تقييم درجاتها](https://arabroot.io/content/images/2026/10/canvas-lms-06-student-grades.webp)

صفحة الطالب: الدرجة 8 من 10 تظهر «10 / 8» بسبب اتجاه الكتابة

## العربية واتجاه الكتابة RTL بصراحة

الواجهة كلها من اليمين إلى اليسار، والقوائم وصفحات المقرر والمحرر بالعربية، وفي `config/locales` داخل ال Image 48 ملف ترجمة، منها `ar.yml`، ولكن أثناء التجربة وجدنا التفاصيل التالية:

- **الكسور مقلوبة:** في صفحة الطالب ظهرت الدرجة 8 من 10 بالشكل «10 / 8»، والإجمالي «10.00 / 8.00»، لأن الرقمين يعرضان داخل نص من اليمين إلى اليسار دون عزل اتجاه، وهذا أول ما سوف يسأل عنه الطالب، فاشرحه للمعلمين قبل أول فصل.
- **أسماء افتراضية بالإنجليزية:** مجموعة المهام الافتراضية اسمها «Assignments»، فغير اسمها من صفحة المهام في كل مقرر أو من قالب المقرر.
- **المصطلح:** القائمة العامة تسمي المقررات «المساقات»، بينما صفحة المقرر تقول «المهام» و«الوحدات»، وكلمة «Table» في قائمة المحرر بقيت بالإنجليزية.
- **اللغة لكل مستخدم:** ضبطنا اللغة العربية لكل مستخدم بالحقل `user[locale]`، وللمقرر بالحقل `course[locale]`، فصفحة الدخول تتبع لغة المتصفح، وما بعد الدخول يتبع اختيار المستخدم.

وأما محرر المحتوى الغني فقد ظهر وعمل في صفحة تحرير الواجب كما في الصورة التالية، مع أننا لم نشغل خدمة `canvas-rce-api`، ولكن دليل الإنتاج يقول إنها مطلوبة لكامل ميزاته، فلم نجرب رفع الصور ومستعرض الملفات من داخله:

![صفحة تحرير الواجب في Canvas بالعربية: اسم المهمة واجب شرح سجلات DNS، ومحرر المحتوى بقوائم تحرير وعرض وإدخال والتنسيق والأدوات وكلمة Table بالإنجليزية، والنص اشرح الفرق بين السجل A والسجل CNAME بمثال من نطاق example.com، وعداد 11 من الكلمات، والنقاط 10، ومجموعة المهام Assignments، وعرض التقدير كنقاط، وعلامة تم النشر](https://arabroot.io/content/images/2026/10/canvas-lms-04-assignment-editor.webp)

محرر المحتوى يعمل بالعربية، وبعض قوائمه بقيت بالإنجليزية

## كم يستهلك من الذاكرة RAM؟

بعد التجربة، وبلا مستخدمين متصلين، قسنا الذاكرة بالأمر `docker stats --no-stream`:

| ال Container | الذاكرة  | ملاحظة                                                                  |
| ------------ | -------- | ----------------------------------------------------------------------- |
| web          | 1.378GiB | عمليتا Passenger بنحو 800MB لكل منهما مع عملية التحميل المسبق Preloader |
| jobs         | 1.034GiB | عاملان Workers للمهام المؤجلة                                           |
| db           | 143MiB   | قاعدة البيانات 38MB                                                     |
| caddy        | 27MiB    |                                                                         |
| redis        | 9MiB     |                                                                         |

المجموع نحو 2.6GB والمنصة خاملة وبعمليتي ويب فقط، وكل عملية ويب إضافية تحتاج نحو 800MB، وهذا ما يجعل 8GB في الدليل حداً أدنى معقولاً لسيرفر واحد، ويجعل Canvas أثقل بكثير من Chamilo الذي احتاج نحو 550MB.

## النطاق Domain والشهادة HTTPS

في الإنتاج غير السطر الأول في `Caddyfile` إلى نطاقك، واحذف `tls internal` فيصدر Caddy شهادة Let's Encrypt تلقائياً كما في [تثبيت Caddy](https://arabroot.io/articles/%D8%AA%D8%AB%D8%A8%D9%8A%D8%AA-caddy-reverse-proxy/)، واكتب النطاق نفسه في `domain.yml`. والدليل يذكر أيضاً `files_domain`، وهو نطاق ثان مختلف لخدمة الملفات المرفوعة، وهو مطلوب إذا أردت أن تكون الملفات آمنة أو أن تسمح بكود JavaScript مخصص في القوالب، لأن الملف المرفوع على النطاق نفسه يمكن أن يقرأ جلسة المستخدم. وهذه خطوات لم نجربها محلياً لأنها تحتاج نطاقاً حقيقياً.

## التحقق Verification من النجاح

- صفحة `/login/canvas` ترجع 200 عبر HTTPS، وترويسات `Set-Cookie` فيها `secure`.
- الدخول بحساب المدير ينجح دون رسالة «رمز مصادقة غير صالح».
- سجل ال Container `jobs` يظهر استعلامات `Delayed::Backend::ActiveRecord::Job` بشكل دوري، أي أن العامل يبحث عن مهام.
- طالب يسلم واجباً، ودرجته تظهر في سجل الدرجات وفي صفحته.

## النسخ الاحتياطي Backup والاستعادة Restore

النسخة الكاملة لـ Canvas ثلاثة أشياء: قاعدة البيانات، والملفات المرفوعة في ال Volume `files` أو في S3، وملفات `config` مع `.env`، وأهمها مفتاح التشفير، فنسخة قاعدة البيانات دونه لا تكفي لاستعادة البيانات المشفرة:

```bash
docker exec edu1-canvas-db sh -c 'pg_dump -U canvas -Fc canvas_production > /tmp/canvas.dump && ls -la /tmp/canvas.dump'
```

```text
-rw-r--r-- 1 root root 2012583 Oct 22 04:54 /tmp/canvas.dump
```

أي 2MB لقاعدة بيانات حجمها 38MB، والصيغة `-Fc` تسمح باستعادة جداول محددة بأمر `pg_restore`، ثم انقل النسخ خارج السيرفر واختبر استعادتها كما في [النسخ الاحتياطي بقاعدة 3-2-1 واختبار الاستعادة](https://arabroot.io/articles/%D8%A7%D9%84%D9%86%D8%B3%D8%AE-%D8%A7%D9%84%D8%A7%D8%AD%D8%AA%D9%8A%D8%A7%D8%B7%D9%8A-%D8%A8%D9%82%D8%A7%D8%B9%D8%AF%D8%A9-3-2-1-%D9%88%D8%A7%D8%AE%D8%AA%D8%A8%D8%A7%D8%B1-%D8%A7%D9%84%D8%A7%D8%B3%D8%AA%D8%B9%D8%A7%D8%AF%D8%A9/).

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

التحديث في Canvas هو بناء جديد للفرع `prod` بعد أن يتحدث، ثم `db:migrate` قبل تشغيل ال Containers الجديدة، ثم `brand_configs:generate_and_upload_all` كما يذكر الدليل في قسم التحديثات اللاحقة، والخطوات بالترتيب:

1. خذ نسخة كاملة، وابن ال Docker Image الجديد بوسم جديد ولا تكتب فوق القديم، حتى تستطيع الرجوع.
2. شغل `bundle exec rake db:migrate` من ال Image الجديد على نسخة من قاعدة البيانات أولاً.
3. غير الوسم في ملف Compose، وشغل `docker compose up -d`، ثم ولد القوالب.

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

## متى تختار Canvas المفتوح؟

- **نقاط القوة:** واجهة يعرفها كثير من الطلاب، وواجهة برمجية REST API شاملة وموثقة، وLTI ناضج، والوحدات Modules وسجل الدرجات واضحة للمعلم، والكود الأساسي هو نفسه الذي تعمل عليه النسخة المستضافة.
- **نقاط الضعف:** المستودع العام لم يتحدث منذ 30 أبريل 2026، وفرع `prod` احتاج تعديلين ليبنى، والبناء ساعة على الأقل وال Docker Image بحجم 10.7GB، والمنصة الخاملة تحتاج 2.6GB، وأهم الإضافات مثل New Quizzes غير متاحة، ولا يوجد دعم رسمي، وفي العربية كسور مقلوبة وأسماء افتراضية بالإنجليزية.

فإذا كانت لديك جامعة بفريق يعرف Rails ويريد Canvas تحديداً فهذا الدليل يوصلك إلى منصة تعمل، ولكن قارن قبل ذلك بتكلفة النسخة المستضافة، وبـ [Moodle](https://arabroot.io/articles/%D8%AA%D8%AB%D8%A8%D9%8A%D8%AA-moodle-%D9%85%D9%86%D8%B5%D8%A9-%D8%A7%D9%84%D8%AA%D8%B9%D9%84%D9%85-%D8%B9%D9%84%D9%89-%D8%B3%D9%8A%D8%B1%D9%81%D8%B1%D9%83/) الذي يصدر بانتظام بإصدارات LTS ودعم أمني معلن.

## مشكلات شائعة وحلولها Troubleshooting

- **`ENOTEMPTY ... /root/.npm/_cacache` أثناء البناء:** استبدل `npm cache clean --force` بحذف المجلد.
- **`Can't resolve '@instructure/platform-alerts'`:** الحزمة غير معلنة في `package.json` في فرع `prod`، فأضفها قبل `compile_assets`.
- **`A timeout occurred while starting a preloader process`:** ارفع `PASSENGER_STARTUP_TIMEOUT`، وتوقع أن يأخذ أول طلب بعد التشغيل دقائق.
- **«رمز مصادقة غير صالح» عند الدخول:** أنت على HTTP، وCanvas في الإنتاج يفرض HTTPS.
- **رسائل `yarn install` عن مشكلة في الاتصال بالشبكة:** الخطوة تحمل آلاف الحزم، وملف البناء يعيد المحاولة تلقائياً بخيار `--network-concurrency 1`، فاتركها تكمل.

## تنظيف المختبر

```bash
docker compose down -v
docker image rm edu1-canvas:prod-2026-04-22 pgvector/pgvector:0.8.7-pg14 redis:7.4.10
```

```text
 Volume edu1-canvas-files Removed
 Network edu1-canvas_default Removing
 Volume edu1-canvas-caddy Removed
 Volume edu1-canvas-db Removed
 Network edu1-canvas_default Removed
```

ولاحظ أن ذاكرة البناء المؤقتة Build Cache تبقى بعد حذف ال Image، وحجمها كبير بعد بناء Canvas، فراجعها بالأمر `docker system df`، ولا تحذفها بأوامر الحذف الجماعي Prune على سيرفر يبني عليه آخرون.

## تدريب عملي

- أنشئ اختباراً كلاسيكياً Classic Quiz بالواجهة البرمجية، وتأكد من ظهوره للطالب، ثم ابحث في الإعدادات عن New Quizzes ولاحظ ما يحدث.
- غير اسم مجموعة المهام «Assignments» إلى «الواجبات» بطلب `PUT` إلى `/api/v1/courses/1/assignment_groups/:id`، ثم افتح صفحة الطالب مرة أخرى.

## الخلاصة

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

- Canvas المفتوح هو الكود الأساسي نفسه بترخيص AGPLv3، ودون إضافات مثل New Quizzes وCatalog وNew Analytics التي تبقيها Instructure لنسختها المستضافة.
- المستودع العام لم يتحدث منذ 30 أبريل 2026، وفرع `prod` احتاج تعديلين ليبنى، فتابعه قبل أن تقرر.
- البناء من `Dockerfile.production` يعطي Docker Image بحجم 10.7GB، والتشغيل يحتاج PostgreSQL وRedis وعملية للمهام وHTTPS إلزامياً.
- المنصة الخاملة تحتاج نحو 2.6GB، وكل عملية ويب إضافية نحو 800MB.
- الواجهة البرمجية REST API تغطي المقررات والمستخدمين والتسجيل والتسليم والتصحيح، وهي أقوى ما فيه للتكامل مع أنظمة الجامعة.
- النسخة الكاملة هي قاعدة البيانات والملفات وملفات الإعداد، ومفتاح التشفير أهمها.

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

- أكتوبر 2026: كتابة الدليل وتجربته على Canvas LMS من فرع prod (stable/2026-04-22) مع PostgreSQL 14 وRedis 7.4.10 وCaddy 2.11.7.