SSD Nodes Learn Hosting plans →
الأدلة Matt Connorبقلم Matt Connor

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

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

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

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

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

تم التحقق من كل ما يرد هنا بالاستناد إلى وثائق الاستضافة الذاتية لـAFFiNE وملفات الإصدار المنشورة في 8 August 2026. كان أحدث إصدار مستقر في ذلك التاريخ هو 0.27.3، وقد نُشر في 23 July 2026. وأُعيد التحقق من الادعاءات أدناه التي تحمل تاريخاً لاحقاً بالاستناد إلى مصادرها الأولية في 12 October 2026، عندما كان أحدث إصدار مستقر هو 0.27.4، وقد نُشر في 18 August 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، وليس من تفضيل معين. في الإصدار v0.27.4، ما زال schema.prisma يعرّف extensions = [pgvector(map: "vector")]، لكنه لا يحتوي على أعمدة embedding. أما النماذج الأربعة التي تحتوي على أعمدة embedding من النوع vector(1024) في v0.27.3، فقد أزيلت من v0.27.4. استخدم صورة pgvector/pgvector:pg16 المرفقة؛ فما زال الامتداد معرّفاً، بينما لا تتضمن صورة postgres:16 القياسية هذا الامتداد.

يستخدم القالب الرسمي للإصدار v0.27.4 صورة pgvector، في حين كان قالب v0.21.0 لا يزال يستخدم postgres:16. في التثبيت الأقدم، لا يكفي تعديل سطر الصورة لإجراء الترقية. لذلك اقرأ صفحة الترقية في وثائق 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. تتطلب الترقية بين الإصدارات الرئيسية أحد المسارات الثلاثة المذكورة في ملاحظات إصدار PostgreSQL 17: تفريغ البيانات واستعادتها في دليل بيانات جديد، أو pg_upgrade، أو النسخ المتماثل المنطقي. في تثبيت Compose واحد، يكون التفريغ والاستعادة عادةً أبسط خيار، لأن pg_upgrade يحتاج إلى وجود ملفات تنفيذ الخادم القديم والجديد جنباً إلى جنب.

ما مقدار 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، قد تستنفد هذه الذروة، مع Postgres وكل العمليات الأخرى الموجودة في الذاكرة، الذاكرة المتاحة. عندئذ تنهي نواة النظام عملية لتحرير الذاكرة باستخدام قاتل نفاد الذاكرة (OOM). ويُرجَّح أن يكون خادم AFFiNE هو العملية المتأثرة لأنه أكبر عملية، مع أن توثيق Docker يوضح أن اختيار أي عملية ممكن، بما في ذلك Docker نفسه.

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

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

رسم بيانيPublished AFFiNE storage figures, August 2026
البيانات وراء هذا الرسم البياني
[
  {
    "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}، بينما كانت الصفحة المرجعية في الوثائق تعرض بنية أحدث تضع بيانات التخزين وPostgreSQL تحت ./data، وتُبقي الإعدادات تحت ./config، ولا تحتاج إلى .env إطلاقاً. نقل الإصدار 0.27.4 الملف المرفق إلى هذه البنية، وفي 12 October 2026 أصبح الملفان متطابقين. إذا ثبّتَّ البرنامج من الملف الأقدم، فلا تزال مساراتك تأتي من .env، وتحذّر صفحة ترقية AFFiNE من أن استبدال ملف Compose من دون نقل هذه الإعدادات قد يؤدي إلى تشغيل Postgres مع دليل بيانات فارغ. يزيل إنشاء الملف بنفسك هذا الالتباس بالكامل، وستحتاج إلى تعديله على أي حال لتثبيت صور الحاويات وتعيين كلمة مرور لقاعدة البيانات.

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 فقط. تسمح إصدارات Docker الأقدم من 28.0.0 للمضيفين الموجودين في مقطع L2 نفسه بالوصول إلى المنافذ المنشورة على localhost، كما يمكن للتوجيه المباشر المكوَّن أن يعرّض منفذ الحاوية رغم النشر على loopback. استخدم Docker 28.0.0 أو إصداراً أحدث، وأبقِ التوجيه المباشر معطّلاً أثناء الإعداد. يربط '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 --all

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

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 وprivate.key اللذين أنشأتهما مهمة الترحيل.

يستخدم المشروع المصدر bind mounts هنا بدلاً من named volumes، وهذا اختيار مقصود: يمكنك ضغط هذه المسارات ونسخها باستخدام أوامر عادية، من دون أن تطلب من 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
tar czf backup/config-$(date +%F).tgz config docker-compose.yml .env

احتفظ بدليل config بالكامل، وملف Compose، و.env في نسختك الاحتياطية، لأن التفريغ لا يحتوي عليها. تنشئ مهمة الترحيل أيضاً config/private.key، لذلك يؤدي نسخ config.json فقط إلى استبعاد ذلك الملف. تُخزَّن الإعدادات المحفوظة في لوحة الإدارة في قاعدة البيانات، لذلك يتضمنها التفريغ أعلاه بالفعل. واعتباراً من 12 October 2026، تتيح صفحة النسخ الاحتياطي في AFFiNE أيضاً تصدير إعدادات الخادم المعدّلة من لوحة الإدارة. انسخ الملفات الثلاثة كلها إلى خارج الخادم. لا تُعد النسخة الاحتياطية المحفوظة على القرص نفسه الذي تحميه نسخة احتياطية. هذا الفصل بين تفريغ قاعدة البيانات وملف tar لدليل الملفات المرفوعة هو النمط الذي ينبغي تكراره مع كل حاوية أخرى ذات حالة تشغّلها. وهو النمط نفسه الذي يحافظ على سجل المحادثات والمرفقات عند استضافة Chatwoot ذاتياً كمنصة دعم.

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

اقرأ خطوات الاستعادة الرسمية قبل أن تحتاج إليها، واقرأها بعناية. في النسخة المنشورة في August 2026، كانت تنسخ ملفاً باسم affine.backup إلى الحاوية ثم تستعيد منه باستخدام ./pg.backup، وهما اسمان مختلفان. كما كانت تزيل دليلاً باسم ./postgres، بينما يحتفظ ملف compose الحالي ببياناته في ./data/postgres. اعتباراً من 12 October 2026، تستخدم الصفحة affine.backup في جميع المواضع، وتطلب الاحتفاظ بدليل 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. وتصف الوثائق التخزين غير المحدود للبيانات الثنائية وحجم البيانات الثنائية غير المحدود للمثيلات المستضافة ذاتياً بأنهما مخططان لكنهما لم يُنفّذا بالكامل بعد، وفق التحقق الذي أُجري في October 2026. لا يهم أيٌّ من هذين الأمرين في منزل أو فريق صغير. لكن كليهما مهم إذا كنت تخطط لنقل أربعين شخصاً.

الترقيات

اقرأ ملاحظات الإصدار أولاً، خصوصاً عند الانتقال بين إصدارين فرعيين مثل 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 على أي صورة. وابتداءً من الإصدار 0.27.4، غيّر قالب compose الرسمي مسارات Postgres والرفع والإعدادات، كما غيّر اتصال قاعدة البيانات الافتراضي. لذلك قارن الملف الجديد بملفك بدلاً من استبداله في تثبيت يعمل.

FAQ

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

تعلن خدمة affine عن condition: service_completed_successfully في مهمة affine_migration، لذلك إذا انتهت عملية الترحيل بأي حالة غير 0، فلن يبدأ الخادم ولن تظهر واجهة الويب إطلاقاً. شغّل docker compose logs affine_migration لمعرفة الخطوة التي توقفت. تحقّق من أن ملف Compose الذي عُدّل يدوياً ما زال يستخدم pgvector/pgvector:pg16: لا يتضمن Postgres القياسي امتداد pgvector الذي ما زال مخطط AFFiNE يعلنه، رغم أن الإصدار v0.27.4 لم يعد يعرّف النماذج الأربعة التي كانت موجودة في الإصدار v0.27.3.

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

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

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

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

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

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

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

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

هل كان هذا المقال مفيدًا؟