SSD Nodes Learn Hosting plans →
الأدلة Matt Connorبقلم Matt Connor · آخر تحديث في 2026-08-27

استضافة AFFiNE ذاتياً عبر Docker Compose

شغّل AFFiNE على خادم VPS واحد عبر Docker Compose، مع أربع حاويات ووسوم صور مثبتة ومسارات البيانات والنسخ الاحتياطية وما يقدمه 2 GB من الذاكرة فعلياً.

ما تحصل عليه عند استضافة AFFiNE ذاتياً

تمنحك استضافة AFFiNE ذاتياً مساحة عمل شبيهة بـNotion على خادم تتحكم فيه، وتعمل في أربعة containers: التطبيق، ومهمة ترحيل تُنفَّذ مرة واحدة، وPostgres، وRedis. وتتضمن التعاون في الوقت الفعلي، بحد أقصى 10 مقاعد تحصل عليها مساحة العمل المستضافة ذاتياً افتراضياً. يتطلب التثبيت ملف compose واحداً وملف إعداد JSON واحداً. أما الجوانب التي تحتاج إلى تخطيط فهي وسوم الصور، وتخطيط القرص، والحد الأقصى للذاكرة، والـproxy الذي تضعه أمام الخدمة.

يجمع AFFiNE محرر المستندات ولوحة لا نهائية في مساحة العمل نفسها، لذلك يمكن قراءة الصفحة كمستند أو بسطها كلوحة بيضاء. إذا كنت لا تزال تقرر ما الذي ستشغّله، فاقرأ أولاً مقارنة بدائل Notion المستضافة ذاتياً. يفترض هذا الدليل أنك اتخذت القرار، ويركّز على تشغيل AFFiNE بطريقة صحيحة بدلاً من مقارنته مرة أخرى.

تم التحقق من كل ما يرد هنا بالاستناد إلى وثائق AFFiNE الخاصة بالاستضافة الذاتية وملفات الإصدار المنشورة في 8 August 2026. كان أحدث إصدار مستقر في ذلك التاريخ هو 0.27.3، وقد نُشر في 23 July 2026.

ما الذي تفعله الحاويات الأربع فعلياً

affine هو الخادم وعميل الويب في صورة واحدة. ويستمع على المنفذ 3010.

affine_migration مهمة تُنفَّذ مرة واحدة وتشغّل node ./scripts/self-host-predeploy.js، وتطبّق عمليات ترحيل قاعدة البيانات، ثم تنتهي. يعلن التطبيق عن اعتماده على condition: service_completed_successfully، لذلك يعني انتهاء عملية الترحيل بحالة غير صفرية أن affine لن يبدأ إطلاقاً. عندما لا تعمل واجهة الويب، يكون سجل تلك المهمة أول ما يجب قراءته.

postgres يحتوي على مستنداتك ومستخدمينك ومساحات العمل وصلاحياتك. الصورة المرفقة هي pgvector/pgvector:pg16، وهي Postgres 16 عادي مع تجميع امتداد pgvector ضمنها. يضيف pgvector نوع عمود vector إلى Postgres، وهو التمثيل الرقمي المستخدم لتخزين embeddings حتى يمكن البحث عن النص وفقاً لمعناه.

redis اعتماد إلزامي: ينتظر كل من الخادم ومهمة الترحيل فحص سلامته قبل البدء. لاحظ ما لا يمنحه ملف compose المرفق إلى Redis، وهو volume. لا يبقى أي شيء داخله بعد docker compose down، وهذا يوضح أنه لا يحتوي على أي محتوى خاص بك ولا يحتاج إلى نسخة احتياطية.

لماذا صورة Postgres هي pgvector وليست postgres القياسية

ينبع هذا المتطلب من مخطط AFFiNE، وليس من تفضيل معيّن. في schema.prisma، يعرّف مصدر البيانات extensions = [pgvector(map: "vector")]، وتحتوي أربعة جداول على عمود embedding من النوع vector(1024). تنشئ مهمة الترحيل هذه الجداول سواء فعّلت ميزات الذكاء الاصطناعي أم لا، لذلك يجب أن يكون الامتداد موجوداً مسبقاً في قاعدة البيانات قبل اكتمال الترحيل. إذا استبدلت الصورة بـpostgres:16، فسيختفي الامتداد، ولن تتمكن مهمة الترحيل من إنشاء تلك الأعمدة، وسيظل الخادم منتظراً مهمة فشلت.

انتقلت AFFiNE إلى صورة pgvector في الإصدار 0.21. إذا كان التثبيت أقدم من ذلك، فلا يكفي تعديل سطر الصورة لإجراء الترقية، لذلك اقرأ صفحة الترقية في وثائق الاستضافة الذاتية لـAFFiNE قبل سحب أي شيء.

هناك نقطة أخرى تتعلق بهذه الوسمة. تعني pg16 استخدام Postgres 16، ولا يمكنك تغيير الإصدار الرئيسي لـPostgres بمجرد تعديل الرقم. إذا غيّرته إلى pg17 فوق دليل بيانات موجود، فسيرفض Postgres التشغيل، وستظهر رسالة مثل The data directory was initialized by PostgreSQL version 16, which is not compatible with this version 17 في docker compose logs postgres. تتطلب الترقية بين الإصدارات الرئيسية إنشاء dump ثم استعادته إلى دليل بيانات جديد.

كم يحتاج AFFiNE المستضاف ذاتياً من CPU وRAM

تطلب صفحة متطلبات AFFiNE توفر 4 أنوية CPU على الأقل و2 GB من RAM، وترفع الذاكرة إلى 4 GB عندما تتجاوز مستنداتك 10,000 كلمة. وتوضح الصفحة نفسها أين تُستخدم الذاكرة: في نظام المزامنة ودمج المستندات. وتذكر رقماً واحداً يستحق الحفظ، وهو أن دمج مستند يحتوي على 10,000 تعديل قد يرفع الاستهلاك مؤقتاً إلى 1 GB.

قارن ذلك الآن بخطة سعتها 2 GB ويكتب فيها شخصان. يكون متوسط الاستهلاك مناسباً. يبقى Postgres وعملية Node ضمن الحد مع توفر بعض الذاكرة. لكن المشكلة تظهر عند ذروة الاستهلاك. قد يطلب دمج كبير واحد 1 GB إضافية فوق كل ما هو موجود في الذاكرة، وعندما يعمل الخادم بسعة 2 GB من دون swap، تستجيب آلية kernel المعروفة باسم قاتل نفاد الذاكرة (OOM) لهذا الطلب بإنهاء العملية الأكبر، وهي خادم AFFiNE.

لن يرى زميلك رسالة خطأ. سيرى إعادة تحميل الصفحة، لأن restart: unless-stopped يعيد تشغيل الحاوية خلال ثوانٍ. لا تخمّن السبب، بل تحقّق منه:

docker inspect affine_server --format '{{.State.OOMKilled}} {{.RestartCount}}'
sudo dmesg -T | grep -i -E 'out of memory|killed process'

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

sudo fallocate -l 2G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
free -h

يجب أن يعرض free -h الآن إجمالي swap قدره 2.0Gi. لا تجعل swap AFFiNE سريعاً، وليس هذا هدفه. فهو يحوّل زيادة تستمر ثانية واحدة إلى تأخير بطيء بدلاً من حاوية متوقفة. أما الجانب الآخر من الإصلاح فهو منع Postgres من توسيع ذاكرة التخزين المؤقت داخل المساحة التي يحتاج إليها التطبيق وقت الدمج، وهذا ما تؤديه حدود الذاكرة على خدمة Compose.

من الأسهل بكثير التنبؤ بمساحة التخزين. هذه هي الأرقام التي تنشرها AFFiNE في الصفحة نفسها:

ChartPublished AFFiNE storage figures, August 2026
The data behind this chart
[
  {
    "label": "Server install",
    "gb": 1.5
  },
  {
    "label": "Postgres per 1,000 docs",
    "gb": 0.1
  },
  {
    "label": "Blob store per 1,000 uploads",
    "gb": 10
  }
]

يحتاج تثبيت الخادم إلى 1.5 GB. وتضيف 1,000 مستندات، يحتوي كل منها تقريباً على 1,000 كلمة، 0.1 GB من بيانات Postgres، وهي كمية تكاد لا تُذكر. وتضيف 1,000 ملفات مرفوعة 10 GB، وهذا هو العامل الأساسي كله. هذه أرقام تخطيط منشورة وليست قياسات من نسخة قيد التشغيل، لذلك تعامل معها على أنها توضح الاتجاه لا أنها ضمان. الاتجاه هو المهم: تبقى قاعدة بياناتك صغيرة، بينما تحدد الملفات التي ترفعها احتياجك من مساحة القرص.

اكتب ملف compose بنفسك، وثبّت العلامات

تُنزّل طريقة التثبيت الموثّقة ملفاً جاهزاً باستخدام curl -L -o docker-compose.yml https://github.com/toeverything/AFFiNE/releases/latest/download/docker-compose.yml. وهذا يعمل. لكن من المهم معرفة تفصيل قبل الاعتماد عليه: اعتباراً من 8 August 2026، ما يزال الملف المرفق بالإصدار 0.27.3 يقرأ مساراته من ملف .env، باستخدام ${UPLOAD_LOCATION} و${CONFIG_LOCATION} و${DB_DATA_LOCATION}، بينما تعرض الصفحة المرجعية في الوثائق تخطيطاً أحدث يضع كل شيء ضمن ./data ولا يحتاج إلى .env إطلاقاً. كلا التخطيطين صحيح. يحسم إنشاء الملف بنفسك هذا السؤال، وستحتاج إلى تعديله على أي حال لتثبيت صور الحاويات وتعيين كلمة مرور لقاعدة البيانات.

mkdir -p ~/affine/config ~/affine/data
cd ~/affine
printf 'DB_PASSWORD=%s\n' "$(openssl rand -hex 24)" > .env
chmod 600 .env

يقرأ Compose الملف .env تلقائياً من دليل المشروع، ويستبدل ${DB_PASSWORD} نيابةً عنك. لذلك لا تظهر كلمة المرور في الملف الذي قد تلصقه في موضوع دعم. احرص على اتباع هذه الممارسة في كل stack تديره، والسبب موضح في إبقاء الأسرار خارج ملف compose.

اكتب الآن ~/affine/docker-compose.yml:

name: affine
services:
  affine:
    image: ghcr.io/toeverything/affine:stable
    container_name: affine_server
    ports:
      - '127.0.0.1:3010:3010'
    depends_on:
      redis:
        condition: service_healthy
      postgres:
        condition: service_healthy
      affine_migration:
        condition: service_completed_successfully
    volumes:
      - ./data/storage:/root/.affine/storage
      - ./config:/root/.affine/config
    environment:
      - REDIS_SERVER_HOST=redis
      - DATABASE_URL=postgresql://affine:${DB_PASSWORD}@postgres:5432/affine
      - AFFINE_INDEXER_ENABLED=false
    restart: unless-stopped

  affine_migration:
    image: ghcr.io/toeverything/affine:stable
    container_name: affine_migration_job
    command: ['sh', '-c', 'node ./scripts/self-host-predeploy.js']
    volumes:
      - ./data/storage:/root/.affine/storage
      - ./config:/root/.affine/config
    environment:
      - REDIS_SERVER_HOST=redis
      - DATABASE_URL=postgresql://affine:${DB_PASSWORD}@postgres:5432/affine
      - AFFINE_INDEXER_ENABLED=false
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy

  redis:
    image: redis:8-alpine
    container_name: affine_redis
    healthcheck:
      test: ['CMD', 'redis-cli', '--raw', 'incr', 'ping']
      interval: 10s
      timeout: 5s
      retries: 5
    restart: unless-stopped

  postgres:
    image: pgvector/pgvector:pg16
    container_name: affine_postgres
    volumes:
      - ./data/postgres:/var/lib/postgresql/data
    environment:
      POSTGRES_USER: affine
      POSTGRES_PASSWORD: ${DB_PASSWORD}
      POSTGRES_DB: affine
      POSTGRES_INITDB_ARGS: '--data-checksums'
    healthcheck:
      test: ['CMD', 'pg_isready', '-U', 'affine', '-d', 'affine']
      interval: 10s
      timeout: 5s
      retries: 5
    restart: unless-stopped

توجد أربعة اختلافات عن الملف الذي تصدره الجهة upstream، ولكل اختلاف سبب.

  • ينشر 127.0.0.1:3010:3010 المنفذ على عنوان loopback فقط، لذلك لا يستطيع أي شيء خارج الخادم الوصول إلى AFFiNE قبل أن تقرر طريقة إتاحته. أما '3010:3010' لدى الجهة upstream فيربط المنفذ بكل الواجهات، ويشمل ذلك الواجهة العامة في معظم صور VPS.
  • أزيل POSTGRES_HOST_AUTH_METHOD: trust، وعُيّنت كلمة مرور بدلاً منه. تقبل مصادقة trust أي اتصال بقاعدة البيانات باستخدام المستخدم affine من دون كلمة مرور. ويقتصر ذلك على شبكة Compose الخاصة، وهذا مناسب إلى أن تربط حاوية أخرى بهذه الشبكة أو تنشر المنفذ 5432 أثناء تصحيح الأخطاء.
  • يستبدل redis:8-alpine قيمة redis المجردة، التي تُحل إلى latest. اعتباراً من August 2026، يكون ذلك Redis 8، ولذلك يحافظ التثبيت على الإصدار الرئيسي الذي اختبرته ويمنع وصول Redis 9 مستقبلاً أثناء تنفيذ docker compose pull لا علاقة له بذلك.
  • يبقى pgvector/pgvector:pg16 مضبوطاً تماماً كما تحدده الجهة upstream، للسبب المذكور أعلاه.

لا يُقرأ POSTGRES_PASSWORD إلا عندما ينشئ Postgres دليل البيانات للمرة الأولى. إذا كان المثيل موجوداً مسبقاً، فعيّن كلمة المرور باستخدام docker compose exec postgres psql -U affine -c "ALTER USER affine WITH PASSWORD 'yourpassword'"، ثم حدّث DATABASE_URL لتطابقها.

يوجد الإعداد في config/config.json

يقرأ AFFiNE إعداداته من config/config.json، وهو المجلد الذي ربطته بالمسار /root/.affine/config. لا ينشئ أي مكوّن هذا الملف تلقائياً، لذلك أنشئه قبل التشغيل الأول. افتح ~/affine/config/config.json في محرر وأضف المحتوى التالي، مع وضع نطاقك بدلاً من المثال:

{
  "$schema": "https://github.com/toeverything/affine/releases/latest/download/config.schema.json",
  "server": {
    "name": "Team workspace",
    "externalUrl": "https://affine.example.com"
  },
  "copilot": {
    "enabled": false,
    "byok": {
      "enabled": false
    }
  }
}

يجب أن تكون قيمة server.externalUrl هي العنوان الذي يفتحه المستخدمون فعلياً في المتصفح. ينشئ AFFiNE روابط المشاركة ودعوات مساحات العمل من هذه القيمة. إذا بقيت على http://localhost:3010، فستشير الدعوة التي ترسلها إلى جهاز المستلم نفسه، وستفشل هناك. اضبطها على عنوان HTTPS العام قبل التشغيل الأول، حتى لا يختلف الملف عن لوحة الإدارة في هذا الإعداد.

تتحكم copilot في ميزات الذكاء الاصطناعي. أما copilot.byok.enabled فهو مفتاح تفعيل استخدام المفتاح الخاص بك، ويتيح لمالك مساحة العمل إدخال مفتاح موفر النموذج الخاص به في إعدادات مساحة العمل. لا تتضمن استضافة AFFiNE ذاتياً اشتراكاً في الذكاء الاصطناعي. اترك كلاً من false إذا لم تكن بحاجة إلى هذه الميزات.

شغّل المكدس:

docker compose up -d
docker compose ps

يجب أن يعرض docker compose ps كلاً من affine_postgres وaffine_redis بالحالة healthy، وأن يعرض affine_server بالحالة running، وأن يعرض affine_migration_job بالحالة exited (0). يجب تتبع أي رمز خروج آخر في مهمة الترحيل، إذ يحدد سجلها الخطوة التي توقفت عندها:

docker compose logs affine_migration

ثبّت صورة الحاوية قبل أن تنسى

stable وسم متحرك. تشير آلية إصدار AFFiNE إلى عدة وسوم لكل إصدار مستقر، ويهمنا منها اثنان هنا: stable، الذي يُعاد توجيهه عند كل إصدار، وstable- المتبوعاً بالاختصار القصير لـ git، وهو لا يتغير. إذا تركت stable كما هو، فسيجلب docker compose pull بعد ستة أشهر صورة مختلفة، ويشغّل عمليات الترحيل على قاعدة بياناتك في وقت لم تختره. ثبّت الصورة التي اختبرتها بالضبط:

docker compose pull
docker image inspect ghcr.io/toeverything/affine:stable --format '{{index .RepoDigests 0}}'

يعرض ذلك سطراً مثل ghcr.io/toeverything/affine@sha256: متبوعاً بقيمة تجزئة طويلة. الصق السلسلة كاملة في سطر image: في كل من affine وaffine_migration. يجب أن تتطابق القيمتان دائماً، لأنهما تمثلان الصورة نفسها أثناء أداء دورين مختلفين. ويعني عدم التطابق ترحيل قاعدة البيانات إلى مخطط واحد ثم تقديم الخدمة باستخدام مخطط آخر. بعد ذلك تصبح الترقية تعديلاً مقصوداً بدلاً من مفاجأة: غيّر قيمة التجزئة، وأنشئ نسخة احتياطية، ثم docker compose pull وdocker compose up -d.

أنشئ حساب المسؤول قبل أن يسبقه أي شخص

افتح /admin على نسخة جديدة، وسيوجّهك AFFiNE إلى صفحة إنشاء حساب لأن الخادم لا يضم حساب مسؤول بعد. لا يتطلب هذا المسار رمز دعوة أو رمز إعداد. يصبح أول شخص يحمّل هذه الصفحة مسؤول خادمك، لذلك يجب أن يظل المنفذ مغلقاً حتى تُسجّل حسابك.

لهذا السبب يربط ملف compose المنفذ بـ 127.0.0.1. اتصل به عبر نفق SSH من جهازك:

ssh -L 3010:127.0.0.1:3010 you@your-server-ip

اترك النفق قيد التشغيل وافتح http://127.0.0.1:3010/admin في المتصفح المحلي. سجّل حسابك وسجّل الدخول، ثم أغلق النفق. عندها فقط يصبح من الآمن إتاحة النسخة عبر اسم عام. تحدث المشكلة نفسها في تطبيقات أخرى تستضيفها بنفسك، وتكون أخطر عندما ينشئ أول تسجيل دخول مفتاح مرور مرتبطاً باسم المضيف. لذلك يجب إعداد TLS والنطاق النهائي قبل إنشاء الحساب الأول عند استضافة openGym بنفسك.

موضع حفظ AFFiNE لبياناتك

تحتوي ثلاثة مسارات على كل شيء، وتقع جميعها داخل الدليل الذي أنشأته.

  • ./data/postgres هو دليل بيانات Postgres: المستندات والمستخدمون ومساحات العمل والأذونات.
  • يُركَّب ./data/storage في /root/.affine/storage داخل الحاوية، ويحتوي على كل ملف مرفوع.
  • يُركَّب ./config في /root/.affine/config، ويحتوي على config.json.

يستخدم المشروع المصدر هنا bind mounts بدلاً من named volumes، وهذا الاختيار مقصود: يمكنك إنشاء أرشيف tar لهذه المسارات ونسخها باستخدام أوامر عادية، من دون أن تطلب من Docker تحديد مكان تخزينها. لكن ملكية الملفات على المضيف تصبح مسؤوليتك، وهذه هي المفاضلة التي يشرحها القسم bind mounts وnamed volumes.

كيفية إجراء نسخة احتياطية من AFFiNE

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

mkdir -p ~/affine/backup
cd ~/affine
docker compose exec -T postgres pg_dump --format c --username affine affine \
  > backup/affine-$(date +%F).dump
ls -lh backup/

يُجرى التفريغ داخل الحاوية عبر مقبسها المحلي، لذلك لا يُطلب منك إدخال كلمة المرور. تحقق من الحجم في خرج ls. يشير وجود ملف حجمه بضع مئات من البايتات إلى فشل التفريغ مع إنشاء الصدفة للملف رغم ذلك. هذا هو نوع الفشل الذي يكتشفه الأشخاص بعد ستة أشهر. ويهمك -T أيضاً: فمن دونه يمكن لـCompose تخصيص طرفية وإتلاف الدفق الثنائي.

الملفات التي حمّلتها هي مجرد ملفات، لذا أنشئ أرشيف tar لها:

tar czf backup/storage-$(date +%F).tgz -C data storage
cp config/config.json backup/config-$(date +%F).json

احتفظ بـ config.json يدوياً ضمن نسختك الاحتياطية. ما تزال وثائق AFFiNE تذكر أن تصدير الإعدادات من لوحة الإدارة غير منفّذ، وفق التحقق الذي أُجري في August 2026، لذلك فالملف الموجود على القرص هو النسخة الوحيدة من إعداداتك. انسخ الملفات الثلاثة جميعها خارج الخادم. لا تُعد النسخة الاحتياطية الموجودة على القرص نفسه الذي تحميه نسخةً احتياطية. هذا الفصل بين تفريغ قاعدة البيانات وأرشيف tar لمجلد uploads هو النمط الذي ينبغي تكراره مع كل حاوية أخرى ذات حالة تشغّلها، وهو النمط نفسه الذي يحافظ على سجل المحادثات والمرفقات عند استضافة Chatwoot ذاتياً كمكتب دعم.

استعادة البيانات، ومطبّ واحد في الخطوات المنشورة

اقرأ خطوات الاستعادة الرسمية قبل أن تحتاج إليها، واقرأها بعناية. في النسخة المنشورة في أغسطس 2026، تنسخ الخطوات ملفاً باسم affine.backup إلى الحاوية، ثم تستعيد البيانات من ./pg.backup، وهما اسمان مختلفان. كما تحذف دليلاً باسم ./postgres، بينما يحتفظ ملف Compose الحالي بالبيانات في ./data/postgres. اتبع المسارات التي استخدمتها فعلياً، لا المسارات الواردة في المقتطف. إليك التسلسل المناسب للتخطيط المستخدم في هذا الدليل:

cd ~/affine
docker compose down
sudo mv data/postgres data/postgres.old
docker compose up -d postgres
docker compose cp backup/affine-2026-08-08.dump postgres:/tmp/affine.dump
docker compose exec postgres pg_restore --format c --username affine \
  --dbname affine --verbose /tmp/affine.dump
docker compose up -d

لاحظ استخدام mv بدلاً من rm. تؤدي الاستعادة فوق قاعدة بيانات لم تحتفظ بنسخة منها إلى تحويل أمر واحد خاطئ إلى فقدان كامل للبيانات. كما أن نقل الدليل القديم إلى جانب آخر لا يكلّف شيئاً. استعد الملفات المرفوعة أيضاً باستخدام tar xzf backup/storage-2026-08-08.tgz -C data، وإلا فستظهر كل وثيقة بمرفقات معطّلة. بعد ذلك، سجّل الدخول وافتح وثيقة تحتوي على صورة. هذا هو الاختبار. فالاستعادة التي لم تفتحها في المتصفح هي ملف، وليست نسخة احتياطية.

وضع AFFiNE خلف وكيل عكسي تستخدمه مسبقاً

يستخدم AFFiNE بروتوكول WebSocket، وهذا غير اختياري. توضح الوثائق ذلك صراحة: يعتمد نظام المزامنة والتعاون في AFFiNE على WebSocket، لذلك فإن الوكيل الذي لا يرقّي هذه الاتصالات يمنحك مساحة عمل يتوقف فيها التحرير عن المزامنة دون إشعار واضح. تُحمّل الصفحة، ويعمل تسجيل الدخول، لكن التعديل الذي تجريه في أحد المتصفحات لا يصل إلى المتصفح الآخر. افتح علامة التبويب Network في أدوات المطوّر في المتصفح، ثم صفِّ النتائج إلى WS. إذا كان الاتصال يفتح ويُغلق مراراً، فهذا يعني أن الوكيل لا يمرّر طلب الترقية.

إذا كنت تستخدم Traefik مسبقاً مع حاويات أخرى، فأضف AFFiNE إليه كخدمة عادية. احذف الكتلة ports: من الخدمة affine، ثم أضف:

    networks:
      - default
      - proxy
    labels:
      - 'traefik.enable=true'
      - 'traefik.docker.network=proxy'
      - 'traefik.http.routers.affine.rule=Host(`affine.example.com`)'
      - 'traefik.http.routers.affine.entrypoints=websecure'
      - 'traefik.http.routers.affine.tls.certresolver=letsencrypt'
      - 'traefik.http.services.affine.loadbalancer.server.port=3010'

وفي أسفل الملف، بجانب services::

networks:
  proxy:
    external: true

يجب أن يطابق اسم محلّل الشهادة الاسم المعرّف في إعدادات Traefik، أما loadbalancer.server.port فهو منفذ الحاوية 3010، وليس منفذ المضيف. يمرّر Traefik اتصالات WebSocket من دون إعداد إضافي، لذلك لا تحتاج إلى إضافة أي شيء آخر. إذا كانت بقية مكدستك تعمل مسبقاً خلف Authentik لتسجيل الدخول الموحّد، فسيقيّد forward auth middleware على هذا الموجّه الوصول من المتصفح إلى AFFiNE، لكن عطّله إلى أن تختبر تطبيق سطح المكتب؛ إذ لا يملك جلسة متصفح، وسيفشل ببساطة في المزامنة. يوضّح تشغيل Traefik واحد أمام عدة تطبيقات طريقة تشغيل عدة تطبيقات خلف نسخة واحدة منه.

في nginx، يجب أن تطلب الترقية صراحةً:

location / {
    proxy_pass http://127.0.0.1:3010;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    client_max_body_size 100m;
}

تكون قيمة client_max_body_size الافتراضية في nginx هي 1 MB، لذلك يفشل كل رفع يتجاوز حجم صورة صغيرة بحالة 413 من دون هذا السطر، ولا يظهر شيء في سجلات AFFiNE لأن الطلب لم يصل إليها أصلاً. يحتاج Caddy إلى سطر واحد هو reverse_proxy http://127.0.0.1:3010، ويتولى بنفسه الشهادات وترقية اتصالات WebSocket.

ما الذي لا يتضمنه الإصدار المستضاف ذاتياً

كن صريحاً مع نفسك بشأن ذلك قبل أن تنقل فريقاً إليه.

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

يكون البحث الكامل في النص من جهة الخادم معطّلاً في ملف compose المرفق، حيث تكون قيمة AFFINE_INDEXER_ENABLED=false مضبوطة على الخادم وعلى مهمة الترحيل. يتطلب تفعيله إضافة حاوية Manticore Search، ما يعني خدمة خامسة واستخداماً أكبر للذاكرة. على خادم بسعة 2 GB، هذا هو التغيير الذي يتجاوز بك حدود الموارد. يظل البحث داخل العميل يعمل في مساحة العمل المفتوحة لديك.

هناك حدّان يجدر بك معرفتهما قبل دعوة الأشخاص. تُمنح مساحة العمل المستضافة ذاتياً 10 مقاعد كحد أقصى، ويتطلب تجاوز ذلك الحصول على ترخيص Team من AFFiNE. يصف التوثيق التخزين غير المحدود للبيانات الثنائية وحجم البيانات الثنائية غير المحدود للمثيلات المستضافة ذاتياً بأنهما مخططان لكن لم يُنفّذا بالكامل بعد، وذلك وفق التحقق في August 2026. لا يهم أي من هذين الحدين في منزل أو فريق صغير. لكن كليهما مهم إذا كنت تخطط لنقل 40 شخصاً.

الترقية

اقرأ ملاحظات الإصدار أولاً، لا سيما عند الانتقال بين إصدارات فرعية مثل 0.26 إلى 0.27، إذ قد تتضمن هذه الإصدارات تغييرات تكسر التوافق. أنشئ نسخة احتياطية من قاعدة البيانات ودليل التخزين قبل تغيير أي شيء، لأن مهمة الترحيل تعدّل المخطط عند بدء التشغيل التالي ولا يمكن التراجع عنها. بعد ذلك غيّر قيمة digest المثبّتة، وشغّل docker compose pull ثم docker compose up -d، وراقب docker compose logs -f affine_migration حتى ينتهي بنجاح. ينظّف docker image prune الطبقات القديمة بعد ذلك. ملاحظة تاريخية لمستخدمي التثبيتات القديمة جداً: ابتداءً من الإصدار 0.23.0، تغيّر اسم الصورة من affine-graphql إلى affine، لذلك يجب إعادة كتابة أسطر الصور في ملف compose الأقدم من ذلك قبل أن يعثر pull على أي صورة.

FAQ

لماذا لا تبدأ حاوية AFFiNE مطلقاً؟

تعرّف خدمة affine التبعية condition: service_completed_successfully في المهمة affine_migration، لذلك إذا انتهت عملية الترحيل بأي حالة غير 0، فلن يبدأ الخادم ولن تظهر واجهة الويب إطلاقاً. شغّل docker compose logs affine_migration لمعرفة الخطوة التي توقفت. السبب الأكثر شيوعاً في ملف compose عُدِّل يدوياً هو استخدام صورة postgres القياسية بدلاً من pgvector/pgvector:pg16، لأن مخطط AFFiNE يعرّف الامتداد pgvector وينشئ جداول تحتوي على أعمدة vector(1024) لا يستطيع Postgres العادي إنشاءها.

ما مقدار RAM الذي يحتاج إليه AFFiNE المستضاف ذاتياً؟

تطلب صفحة متطلبات AFFiNE ما لا يقل عن 4 أنوية CPU و2 GB من RAM، وترتفع الحاجة إلى 4 GB عندما تتجاوز المستندات 10,000 كلمة. وتذكر الصفحة أن دمج مستند يحتوي على 10,000 تعديلاً قد يرفع الاستهلاك مؤقتاً إلى 1 GB. على خادم بسعة 2 GB، يكون هذا الارتفاع هو ما يسبب المشكلة، وليس الحمل أثناء الخمول: إذ يوقف قاتل نفاد الذاكرة في النواة عملية AFFiNE، ثم تعيد restart: unless-stopped تشغيلها، لذلك يرى المستخدمون إعادة تحميل الصفحة بدلاً من ظهور خطأ. أكّد ذلك باستخدام docker inspect affine_server --format '{{.State.OOMKilled}}' وsudo dmesg -T | grep -i 'out of memory'، ثم أضف ملف swap بسعة 2 GB حتى يصبح الارتفاع بطيئاً بدلاً من أن يكون قاتلاً.

أين يخزّن AFFiNE بياناتي، وما الذي يجب أن أنسخه احتياطياً؟

تحتوي ثلاثة مسارات ضمن مجلد compose على كل شيء: ./data/postgres لقاعدة البيانات، و./data/storage للملفات المرفوعة، و./config لـ config.json. انسخ قاعدة البيانات احتياطياً باستخدام docker compose exec -T postgres pg_dump --format c --username affine affine > affine.dump بدلاً من نسخ الملفات، لأن نسخ ملفات Postgres أثناء تشغيله ليس آمناً. أنشئ أرشيفاً بصيغة tar من ./data/storage للملفات المرفوعة، واحتفظ بنسخة من config.json يدوياً، لأن تصدير الإعدادات من لوحة الإدارة مُدرج على أنه لم يُنفَّذ بعد حتى August 2026.

هل يعمل التعاون في الوقت الفعلي على AFFiNE المستضاف ذاتياً؟

نعم، ولا تحتاج إلى تفعيل أي إعداد له. المتطلب الوحيد هو إعداد reverse proxy، لأن المزامنة تعمل عبر اتصالات WebSocket. في nginx، يعني ذلك proxy_http_version 1.1 إضافةً إلى رأسي Upgrade وConnection: upgrade، بينما يمرر Traefik وCaddy هذه الاتصالات دون إعداد إضافي. علامة proxy الذي لا يرقّي هذه الاتصالات هي أن مساحة العمل تُحمَّل ويتم تسجيل الدخول إليها بشكل طبيعي، لكن التعديلات التي تُجرى في متصفح لا تظهر مطلقاً في متصفح آخر.

هل يمكنني تشغيل AFFiNE باستخدام صورة Postgres قياسية؟

لا. يعرّف schema.prisma الخاص بـAFFiNE extensions = [pgvector(map: "vector")]، ويحدد أربعة جداول تحتوي على عمود embedding من النوع vector(1024). وتنشئ مهمة الترحيل هذه الجداول حتى عند تعطيل ميزات الذكاء الاصطناعي. استخدم pgvector/pgvector:pg16، وهو Postgres 16 مضمّناً فيه هذا الامتداد. وإذا وجّهت AFFiNE إلى خادم Postgres خارجي، فثبّت pgvector عليه وأنشئ الامتداد في قاعدة البيانات المستهدفة قبل تشغيل الترحيل.