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

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

المقرر والمستخدمون عبر 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'})والصورة التالية للمقرر كما تراه المعلمة، بالوحدة والواجب، والقائمة الجانبية بالعربية:

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


العربية واتجاه الكتابة RTL بصراحة
الواجهة كلها من اليمين إلى اليسار، والقوائم وصفحات المقرر والمحرر بالعربية، وفي config/locales داخل ال Image 48 ملف ترجمة، منها ar.yml، ولكن أثناء التجربة وجدنا التفاصيل التالية:
- الكسور مقلوبة: في صفحة الطالب ظهرت الدرجة 8 من 10 بالشكل «10 / 8»، والإجمالي «10.00 / 8.00»، لأن الرقمين يعرضان داخل نص من اليمين إلى اليسار دون عزل اتجاه، وهذا أول ما سوف يسأل عنه الطالب، فاشرحه للمعلمين قبل أول فصل.
- أسماء افتراضية بالإنجليزية: مجموعة المهام الافتراضية اسمها «Assignments»، فغير اسمها من صفحة المهام في كل مقرر أو من قالب المقرر.
- المصطلح: القائمة العامة تسمي المقررات «المساقات»، بينما صفحة المقرر تقول «المهام» و«الوحدات»، وكلمة «Table» في قائمة المحرر بقيت بالإنجليزية.
- اللغة لكل مستخدم: ضبطنا اللغة العربية لكل مستخدم بالحقل
user[locale]، وللمقرر بالحقلcourse[locale]، فصفحة الدخول تتبع لغة المتصفح، وما بعد الدخول يتبع اختيار المستخدم.
وأما محرر المحتوى الغني فقد ظهر وعمل في صفحة تحرير الواجب كما في الصورة التالية، مع أننا لم نشغل خدمة canvas-rce-api، ولكن دليل الإنتاج يقول إنها مطلوبة لكامل ميزاته، فلم نجرب رفع الصور ومستعرض الملفات من داخله:

كم يستهلك من الذاكرة 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، واكتب النطاق نفسه في 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 كما يذكر الدليل في قسم التحديثات اللاحقة، والخطوات بالترتيب:
- خذ نسخة كاملة، وابن ال Docker Image الجديد بوسم جديد ولا تكتب فوق القديم، حتى تستطيع الرجوع.
- شغل
bundle exec rake db:migrateمن ال Image الجديد على نسخة من قاعدة البيانات أولاً. - غير الوسم في ملف 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.