التعليم والبحث العلمي الأدلة التقنية

Canvas LMS المفتوح المصدر Open Source على سيرفرك

ما الذي تحصل عليه من Canvas المفتوح وما الذي يبقى في نسخة Instructure المستضافة، وكيف تبنيه وتشغله وتنشئ أول مقرر.

Canvas LMS المفتوح المصدر Open Source على سيرفرك

كثير من الطلاب الذين درسوا في جامعات خارج المنطقة يعرفون Canvas من نسخته المستضافة، فيأتي السؤال إلى قسم تقنية المعلومات: الكود مفتوح المصدر على GitHub، فلماذا لا نشغله على سيرفراتنا دون رسوم؟ والإجابة المختصرة أن ذلك ممكن، ولكن ما تحصل عليه ليس هو نفسه ما يراه الطالب في النسخة المستضافة، والتثبيت أثقل بكثير من Moodle وChamilo. وفي مقال أنظمة إدارة التعلم LMS ذكرنا أنه يناسب جامعة لديها فريق يعرف Rails، وفي هذا المقال سوف نتناول:

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

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

Canvas LMS تطبيق Ruby on Rails منشور بترخيص AGPLv3، وصفحة الأسئلة الشائعة FAQ في المستودع تشرح نموذج الترخيص بوضوح: 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
الكود الأساسي واحد، والفرق في الإضافات المغلقة والخدمات التي تديرها الشركة

والقائمة التي في الصورة هي كما في صفحة الأسئلة الشائعة: 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 يقول إن آخر نسخة فيه مطابقة لفرع stable/2026-04-22، أي أن المستودع العام لم يتحدث منذ ما يقارب ستة أشهر، ولم نجد إعلاناً رسمياً يشرح السبب. وهذا مهم لأمرين: الأول أن الإصلاحات الأمنية التي تصدر للنسخة المستضافة بعد ذلك التاريخ لا تصل إليك، والثاني أننا سوف نرى بعد قليل أن هذا الفرع نفسه لا يبنى كما هو. لذلك تابع المستودع قبل أن تقرر، ولا تبن خطة جامعة كاملة على كود لا تعرف هل سوف يتحدث.

المتطلبات Requirements

دليل التشغيل في الإنتاج Production Start هو المرجع الرسمي، وأهم ما فيه:

البندكما في الدليل
النظامكتب واختبر على Ubuntu 22.04 LTS
الذاكرة RAM8GB على الأقل، خاصة إذا كان كل شيء على سيرفر واحد
قاعدة البياناتPostgreSQL 14 أو أحدث منذ إصدار stable/2024-11-20
اللغةRuby 3.4.1 أو أحدث، وNode.js 20، وYarn 1.19.1
الخدماتRedis، وبريد صادر SMTP، وعملية مستقلة للمهام المؤجلة Jobs
محرر المحتوىخدمة canvas-rce-api مطلوبة لكامل ميزات محرر المحتوى الغني Rich Content Editor

ويفترض الدليل أنك تعرف إعداد Apache وتطبيقات Rails وPassenger وPostgreSQL، ويحيلك في الدعم إلى قائمة المراسلات المجتمعية، وأما دليل البداية السريعة Quick Start فيقول صراحة إنه لبيئة تطوير وليس للإنتاج، وإن سكربت Docker الذي فيه ينصح بمساحة 150GB على القرص.

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

الطريقة البسيطة هي البحث عن Docker Image جاهز على Docker Hub، ولكن آخر تحديث لـ instructure/canvas-lms هناك كان في يونيو 2019، فهو غير صالح إطلاقاً. والطريقة الثانية هي اتباع دليل الإنتاج خطوة بخطوة على Ubuntu، وهي الطريقة الرسمية. ولكن المستودع نفسه فيه ملف اسمه Dockerfile.production ينفذ الخطوات نفسها: يثبت Node وPostgreSQL Client فوق Docker Image من Instructure فيه Ruby 3.4 وnginx وPassenger، ثم bundle install، ثم yarn install، ثم canvas:compile_assets، لذلك سوف نبني منه.

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

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

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

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

#7 [ 5/10] RUN npm install -g [email protected] && 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»، لذلك استبدلناه بحذف المجلد مباشرة، وهو يؤدي الغرض نفسه:

RUN npm install -g [email protected] && rm -rf /root/.npm

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

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

#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 في الفرع:

RUN (yarn install --frozen-lockfile || yarn install --frozen-lockfile --network-concurrency 1) && yarn add --ignore-workspace-root-check @instructure/[email protected] && \
  bin/rails canvas:compile_assets --trace && \
  rm -rf node_modules

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

--> 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 بقيم عشوائية:

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 المقابل له في المستودع:

# 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: "[email protected]"
  password: "change-me-smtp"
  authentication: "plain"
  domain: "example.com"
  outgoing_address: "[email protected]"
  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

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:

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 ينشئ الجداول وحساب المدير، والدليل يذكر متغيرات البيئة التي تغني عن الأسئلة التفاعلية:

docker compose up -d db redis
docker compose run --rm \
  -e [email protected] \
  -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
==  InitCanvasDb: migrating ===================================================
-- create_extension(:pg_trgm, {schema: "public", if_not_exists: true})
   -> 0.0307s
...
Notifications Loaded
...
Initial data loaded

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

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:

[ 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، وال Docker Image من Instructure يقرأ هذه القيمة من المتغير PASSENGER_STARTUP_TIMEOUT عند توليد ملف nginx، لذلك رفعناها إلى 300 كما في ملف Compose، وخفضنا PASSENGER_MAX_POOL_SIZE من 6 إلى 2 لتوفير الذاكرة. وبعد ذلك أصبح أول طلب يأخذ نحو 3 دقائق، وما بعده أقل من ثانية:

code=200 after 184s
200 0.147354

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

قبل أن نضيف Caddy، كان خادم الويب منشوراً مباشرة على HTTP، فظهرت صفحة الدخول بالعربية، ولكن عند إدخال البريد وكلمة المرور ظهرت الرسالة «رمز مصادقة غير صالح»، أي Invalid Authenticity Token. والسبب في config/environments/production.rb، ففيه config.force_ssl = true، وعندها يجعل Canvas ملفات تعريف الارتباط Cookies آمنة Secure، والمتصفح لا يحفظها على اتصال HTTP، فيضيع رمز الحماية من هجمات CSRF بين الصفحة والطلب. والقاعدة: Canvas في وضع الإنتاج لا يعمل إلا خلف HTTPS، حتى في المختبر. وبعد إضافة Caddy ظهرت الترويسات كما ينبغي:

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

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

صفحة تسجيل الدخول في Canvas بالعربية على خلفية داكنة: شعار CANVAS، وحقلا البريد الإلكتروني وكلمة المرور، وزر تسجيل الدخول، وخيار الإبقاء على تسجيل الدخول، ورابط هل نسيت كلمة المرور، وفي الأسفل نظام LMS مفتوح المصدر وتعليمات وشعار INSTRUCTURE
صفحة الدخول بالعربية، وهي تعمل فقط عبر HTTPS في وضع الإنتاج

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

من أقوى ما في Canvas واجهته البرمجية REST API، فكل ما تفعله في الواجهة تقريباً تفعله بطلب HTTP، وهذا ما تحتاجه الجامعة لربطه بنظام معلومات الطلاب كما شرحنا في الأنظمة التي تحتاجها الجامعة. والخطوة الأولى رمز وصول Access Token للمدير، وأنشأناه من سطر الأوامر، وفي الإنتاج تنشئه من صفحة الإعدادات في حساب المدير:

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 صغير، والطلبات بالترتيب كما في توثيق المقررات:

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'])
1 NET101 available ar
2 سارة المعلمة [email protected]
3 خالد الطالب [email protected]
4 ريم الطالبة [email protected]
1 TeacherEnrollment active
2 StudentEnrollment active
3 StudentEnrollment active

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

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 بعشر نقاط، وقائمة المقرر الصفحة الرئيسية والإعلانات والمهام والمناقشات والدرجات والأشخاص والصفحات والملفات والمناهج والنتائج ومعايير التقييم والاختبارات والوحدات وإعدادات، والقائمة العامة الداكنة فيها الحساب ولوحة المعلومات والمساقات والتقويم وعلبة الوارد وتعليمات
الوحدات Modules هي طريقة Canvas في ترتيب المقرر أسبوعاً بأسبوع

ثم سلم الطالب خالد الواجب، وصححته المعلمة بثماني درجات مع تعليق، وكلاهما عبر الواجهة البرمجية كما في توثيق التسليمات، والخيار as_user_id يجعل المدير ينفذ الطلب نيابة عن الطالب، وهو مفيد في الاختبار وخطير في الإنتاج، فلا تعط رمز المدير لأي سكربت لا تحتاجه:

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')])
[(3, 8.0, 'graded'), (4, None, 'unsubmitted')]

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

دفتر الدرجات في Canvas بالعربية لمقرر NET101: بحث عن الطلاب وبحث عن المهام، وصف خالد الطالب بدرجة 8 في الواجب و80% في الإجمالي، وصف ريم الطالبة بشرطات لأنها لم تسلم
سجل الدرجات: 8 لخالد أي 80%، وشرطة لريم التي لم تسلم
صفحة الدرجات الخاصة بخالد الطالب في Canvas بالعربية: الواجب شرح سجلات DNS تم الإرسال 22 أكتوبر في 7:43 والدرجة معروضة 10 / 8، ومجموعة Assignments بالإنجليزية 80% و10.00 / 8.00، والإجمالي 80%، وفي الجانب الإجمالي 80% وخيار يعتمد الحساب فقط على المهام التي تم تقييم درجاتها
صفحة الطالب: الدرجة 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، وعرض التقدير كنقاط، وعلامة تم النشر
محرر المحتوى يعمل بالعربية، وبعض قوائمه بقيت بالإنجليزية

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

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

ال Containerالذاكرةملاحظة
web1.378GiBعمليتا Passenger بنحو 800MB لكل منهما مع عملية التحميل المسبق Preloader
jobs1.034GiBعاملان Workers للمهام المؤجلة
db143MiBقاعدة البيانات 38MB
caddy27MiB
redis9MiB

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

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

في الإنتاج غير السطر الأول في Caddyfile إلى نطاقك، واحذف tls internal فيصدر Caddy شهادة Let's Encrypt تلقائياً كما في تثبيت Caddy، واكتب النطاق نفسه في 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، وأهمها مفتاح التشفير، فنسخة قاعدة البيانات دونه لا تكفي لاستعادة البيانات المشفرة:

docker exec edu1-canvas-db sh -c 'pg_dump -U canvas -Fc canvas_production > /tmp/canvas.dump && ls -la /tmp/canvas.dump'
-rw-r--r-- 1 root root 2012583 Oct 22 04:54 /tmp/canvas.dump

أي 2MB لقاعدة بيانات حجمها 38MB، والصيغة -Fc تسمح باستعادة جداول محددة بأمر pg_restore، ثم انقل النسخ خارج السيرفر واختبر استعادتها كما في النسخ الاحتياطي بقاعدة 3-2-1 واختبار الاستعادة.

التحديث 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 الذي يصدر بانتظام بإصدارات 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، فاتركها تكمل.

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

docker compose down -v
docker image rm edu1-canvas:prod-2026-04-22 pgvector/pgvector:0.8.7-pg14 redis:7.4.10
 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.
نشرة عرب رووت | ArabRoot

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

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

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

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