SSD Nodes Learn 🎉 VPS من $4.99/شهر
الأدلة Matt Connorبقلم Matt Connor

استضافة AFFiNE ذاتياً: مساحة عمل شبيهة بـNotion

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

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

تمنحك الاستضافة الذاتية لـAFFiNE مساحة عمل شبيهة بـNotion على خادم تتحكم فيه، وتعمل عبر أربع حاويات: التطبيق، ومهمة ترحيل تُنفَّذ مرة واحدة، و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. وهو التمثيل الرقمي المستخدم لتخزين التضمينات، بحيث يمكن البحث في النص وفق معناه.

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. يتطلب الانتقال إلى إصدار رئيسي آخر إنشاء تفريغ ثم استعادته في دليل بيانات جديد.

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

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

قارن ذلك الآن بخطة سعتها 2 GB مع شخصين يكتبان. يكون متوسط الاستهلاك مناسباً. يبقى كل من Postgres وعملية Node ضمن الحد مع توفر مساحة إضافية. لكن المشكلة تظهر عند الذروة. قد يطلب دمج كبير واحد 1 GB فوق كل ما هو موجود في الذاكرة، وعندما يعمل الخادم بسعة 2 GB من دون swap، تستجيب آلية قتل العمليات عند نفاد الذاكرة (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. وتضيف ألف وثيقة، تحتوي كل منها على نحو ألف كلمة، 0.1 GB من بيانات Postgres، وهي كمية تقترب من الصفر. وتضيف ألفة ملفات مرفوعة 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} نيابةً عنك، لذلك لا تظهر كلمة المرور في الملف الذي قد تلصقه في موضوع دعم. من المفيد الحفاظ على هذه العادة في كل مكدس تشغّله، والتفسير موضح في إبقاء الأسرار خارج ملف 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' فيربط المنفذ بكل واجهات الشبكة، ويشمل ذلك في معظم صور 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 بالحالة السليمة، وأن يعرض affine_server قيد التشغيل، وأن يعرض affine_migration_job بالحالة exited (0). أي رمز خروج آخر في مهمة الترحيل هو الخطأ الذي يجب تتبعه، ويسمي سجلها الخطوة التي توقفت:

docker compose logs affine_migration

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

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

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

يطبع ذلك سطراً مثل ghcr.io/toeverything/affine@sha256: متبوعاً بقيمة hash طويلة. الصق السلسلة كاملة في سطر image: في كل من affine وaffine_migration. يجب أن يتطابقا دائماً، لأنهما يستخدمان الصورة نفسها لدورين مختلفين. ويعني عدم التطابق ترحيل قاعدة البيانات إلى مخطط واحد ثم تقديمها باستخدام مخطط آخر. بعد ذلك تصبح الترقية تعديلاً متعمداً لا مفاجأة: غيّر قيمة digest، وأنشئ نسخة احتياطية، ثم 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 في متصفحك المحلي. سجّل الحساب ثم سجّل الدخول، وبعد ذلك أغلق النفق. عندها فقط يصبح من الآمن إتاحة المثيل عبر اسم عام.

أين يحتفظ AFFiNE ببياناتك

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

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

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

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

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

اقرأ خطوات الاستعادة الرسمية قبل أن تحتاج إليها، واقرأها بعناية. وفقاً للنسخة المنشورة في August 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 دون إعداد إضافي، لذلك لا تحتاج إلى إضافة أي شيء آخر. يوضّح استخدام 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 عليه وأنشئ الإضافة في قاعدة البيانات المستهدفة قبل تشغيل الترحيل.