SSD Nodes Learn 🎉 VPS از $5.50/ماه
راهنماها Matt Connorتوسط Matt Connor · به‌روزرسانی شده 2026-08-13

آموزش نصب و میزبانی شخصی AFFiNE با Docker Compose

راهنمای کامل اجرای AFFiNE روی VPS با استفاده از 4 کانتینر. نحوه تنظیم Postgres و Redis، مدیریت فایل‌های پیکربندی، تگ‌های دقیق ایمیج و بررسی عملکرد با 2 GB رم.

مزایای میزبانی شخصی AFFiNE

میزبانی شخصی AFFiNE به شما یک فضای کاری به سبک Notion روی سروری که تحت کنترل خودتان است می‌دهد. این سرویس در قالب چهار کانتینر اجرا می‌شود: برنامه اصلی، یک job برای مهاجرت داده‌ها، Postgres و Redis. قابلیت همکاری بلادرنگ (real-time collaboration) در این نسخه گنجانده شده است و به‌صورت پیش‌فرض تا 10 کاربر در فضای کاری میزبانی‌شده پشتیبانی می‌شوند. نصب آن شامل یک فایل compose و یک فایل پیکربندی JSON است. مواردی که باید به آن‌ها توجه کنید شامل تگ‌های image، ساختار دیسک، محدودیت حافظه و reverse proxy است که در مقابل آن قرار می‌دهید.

AFFiNE یک ویرایشگر سند و یک بوم بی‌نهایت (infinite canvas) را در یک فضای کاری واحد ترکیب می‌کند، بنابراین هر صفحه می‌تواند هم به صورت یک سند خوانده شود و هم به صورت یک تخته‌سفید گسترش یابد. اگر هنوز در حال تصمیم‌گیری برای انتخاب سرویس هستید، ابتدا مقایسه جایگزین‌های Notion برای میزبانی شخصی را مطالعه کنید. این راهنما فرض را بر این می‌گذارد که انتخاب خود را انجام داده‌اید و به‌جای مقایسه مجدد، بر اجرای صحیح AFFiNE تمرکز دارد.

تمام مطالب این بخش با مستندات میزبانی شخصی AFFiNE و فایل‌های منتشرشده در تاریخ 8 August 2026 مطابقت داده شده است. جدیدترین نسخه پایدار در آن تاریخ، 0.27.3 بود که در 23 July 2026 منتشر شد.

عملکرد واقعی چهار کانتینر

affine شامل سرور و کلاینت وب در یک ایمیج واحد است. این کانتینر روی پورت 3010 گوش می‌دهد.

affine_migration یک job یک‌بار‌اجرا (one-shot) است که node ./scripts/self-host-predeploy.js را اجرا کرده، migrationهای دیتابیس را اعمال می‌کند و سپس خارج می‌شود. اپلیکیشن برای این job، condition: service_completed_successfully تعریف کرده است؛ بنابراین اگر migration با وضعیتی غیر از صفر خارج شود، به این معنی است که affine هرگز شروع نخواهد شد. هنگامی که رابط وب بالا نمی‌آید، لاگ این job اولین چیزی است که باید بررسی کنید.

postgres اسناد، کاربران، فضای کاری و دسترسی‌های شما را نگهداری می‌کند. ایمیج ارائه‌شده pgvector/pgvector:pg16 است که در واقع همان Postgres 16 معمولی با افزونه pgvector کامپایل‌شده می‌باشد. pgvector یک نوع ستون vector به Postgres اضافه می‌کند؛ فرمت عددی که برای ذخیره embeddingها استفاده می‌شود تا بتوان متن را بر اساس مفهوم جستجو کرد.

redis یک وابستگی حیاتی است: هم سرور و هم job مربوط به migration، پیش از شروع کار منتظر موفقیت‌آمیز بودن health check آن می‌مانند. توجه کنید که فایل compose ارائه‌شده، برای Redis هیچ volumeای تعریف نکرده است. هیچ داده‌ای در آن پس از docker compose down باقی نمی‌ماند و این به‌وضوح نشان می‌دهد که محتوای شما در آن ذخیره نمی‌شود و نیازی به پشتیبان‌گیری ندارد.

Why the Postgres image is pgvector and not stock postgres

The requirement comes from AFFiNE's schema, not from a preference. In schema.prisma the datasource declares extensions = [pgvector(map: "vector")], and four tables carry an embedding column typed vector(1024). The migration job creates those tables whether or not you ever turn the AI features on, so the extension must already exist in the database before the migration can finish. Swap in postgres:16 and the extension is gone, the migration cannot create those columns, and the server sits there waiting for a job that failed.

AFFiNE moved to the pgvector image at version 0.21. On an install older than that, editing the image line is not the whole upgrade, so read the upgrade page in the AFFiNE self-host docs before you pull anything.

One more thing about that tag. pg16 means Postgres 16, and a Postgres major version is not a number you can bump. Change it to pg17 over an existing data directory and Postgres refuses to start, with a line like The data directory was initialized by PostgreSQL version 16, which is not compatible with this version 17 in docker compose logs postgres. A major version move means a dump and a restore into a fresh data directory.

سرویس self-hosted AFFiNE به چه میزان CPU و RAM نیاز دارد

صفحهٔ نیازمندی‌های AFFiNE حداقل 4 هسته CPU و 2 گیگابایت RAM را توصیه می‌کند و در صورتی که تعداد کلمات اسناد شما از 10,000 کلمه فراتر رود، این مقدار حافظه را به 4 گیگابایت افزایش می‌دهد. همان صفحه توضیح می‌دهد که این حافظه صرف چه کاری می‌شود: سیستم همگام‌سازی (sync) و ادغام اسناد (document merging). یک عدد در این میان ارزش به‌خاطر سپردن دارد: ادغام سندی با 10,000 تغییر می‌تواند تا 1 گیگابایت حافظه مصرف کند.

حال این موضوع را در یک پلن 2 گیگابایتی با دو کاربر در حال نوشتن در نظر بگیرید. میانگین مصرف مشکلی ندارد. Postgres و پردازش Node زیر حد مجاز باقی می‌مانند و فضای خالی نیز وجود دارد. مشکل اصلی، پیک‌های مصرف است. یک ادغام بزرگ می‌تواند علاوه بر حافظهٔ اشغال‌شده، 1 گیگابایت دیگر نیز درخواست کند. در یک سرور 2 گیگابایتی بدون swap، قابلیت OOM (Out-of-Memory) killer در هسته سیستم‌عامل با کشتن بزرگ‌ترین پردازش، یعنی سرور 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 اکنون باید در مجموع 2.0Gi swap را گزارش کند. Swap باعث سریع‌تر شدن AFFiNE نمی‌شود و هدف آن نیز این نیست. Swap یک پیک مصرف یک‌ثانیه‌ای را به یک ثانیه کندی تبدیل می‌کند، نه یک کانتینر ازکارافتاده. بخش دیگر راه‌حل این است که از رشد کش 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 گیگابایت فضا اشغال می‌کند. هزار سند که هر کدام تقریباً هزار کلمه دارند، 0.1 گیگابایت به داده‌های Postgres اضافه می‌کنند که تقریباً ناچیز است. هزار فایل آپلود شده، 10 گیگابایت اضافه می‌کنند که تمام ماجرا همین است. این‌ها ارقام برنامه‌ریزی‌شده هستند و نه اندازه‌گیری‌های دقیق از یک نمونه در حال اجرا، بنابراین آن‌ها را به عنوان یک الگوی کلی در نظر بگیرید، نه یک وعده قطعی. الگو مهم است: پایگاه داده شما کوچک باقی می‌ماند و فایل‌های آپلود شده تعیین‌کننده میزان مصرف دیسک شما هستند.

فایل compose را خودتان بنویسید و تگ‌ها را ثابت کنید

نصب مستندشده، یک فایل آماده را با curl -L -o docker-compose.yml https://github.com/toeverything/AFFiNE/releases/latest/download/docker-compose.yml دانلود می‌کند. این روش کار می‌کند. یک نکته وجود دارد که پیش از تکیه بر آن باید بدانید: تا تاریخ 8 اوت 2026، فایلی که به نسخه 0.27.3 پیوست شده است، همچنان مسیرهای خود را از یک فایل .env و با استفاده از ${UPLOAD_LOCATION}، ${CONFIG_LOCATION} و ${DB_DATA_LOCATION} می‌خواند، در حالی که صفحه مرجع مستندات، ساختار جدیدتری را نشان می‌دهد که همه چیز را تحت ./data نگه می‌دارد و اصلاً نیازی به .env ندارد. هر دو معتبر هستند. نوشتن فایل توسط خودتان این مسئله را حل می‌کند و به‌هرحال برای ثابت کردن (pin) ایمیج‌ها و تعیین رمز عبور دیتابیس، باید آن را ویرایش کنید.

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

چهار تفاوت با فایلی که نسخه اصلی ارائه می‌دهد وجود دارد و هر کدام دلیلی دارند.

  • 127.0.0.1:3010:3010 پورت را فقط روی آدرس loopback منتشر می‌کند، بنابراین تا زمانی که خودتان تصمیم نگیرید، هیچ‌چیز خارج از سرور نمی‌تواند به AFFiNE دسترسی پیدا کند. مقدار '3010:3010' در نسخه اصلی، تمام اینترفیس‌ها را bind می‌کند و در اکثر ایمیج‌های VPS، این شامل اینترفیس عمومی نیز می‌شود.
  • POSTGRES_HOST_AUTH_METHOD: trust حذف شده و به‌جای آن یک رمز عبور تعیین شده است. احراز هویت Trust، هر اتصالی به آن دیتابیس را به‌عنوان کاربر affine بدون رمز عبور می‌پذیرد. این محدود به شبکه خصوصی Compose است که تا روزی که یک کانتینر دیگر به آن شبکه متصل کنید یا پورت 5432 را هنگام دیباگ کردن منتشر کنید، مشکلی ایجاد نمی‌کند.
  • redis:8-alpine جایگزین یک redis ساده شده است که به latest اشاره می‌کند. تا اوت 2026، این نسخه Redis 8 است، بنابراین ثابت کردن (pin) آن باعث می‌شود نسخه اصلی که تست کرده‌اید حفظ شود و از ورود نسخه Redis 9 در طول یک docker compose pull بی‌ارتباط جلوگیری شود.
  • pgvector/pgvector:pg16 دقیقاً همان‌طور که در نسخه اصلی تنظیم شده باقی می‌ماند، به دلیلی که در بالا ذکر شد.

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 mount کرده‌اید. هیچ فرآیندی این فایل را برای شما ایجاد نمی‌کند، بنابراین پیش از اولین اجرا آن را بنویسید. فایل ~/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 لینک‌های اشتراک‌گذاری و دعوت‌نامه‌های فضای کاری (workspace) را بر اساس این مقدار می‌سازد؛ بنابراین اگر آن را روی http://localhost:3010 رها کنید، دعوت‌نامه‌ای که ارسال می‌کنید گیرنده را به ماشین خودش هدایت می‌کند و در آنجا با خطا مواجه می‌شود. پیش از اولین اجرا، آن را روی آدرس عمومی HTTPS تنظیم کنید تا فایل پیکربندی و پنل مدیریت همیشه با هم هم‌خوانی داشته باشند.

گزینه copilot ویژگی‌های هوش مصنوعی را کنترل می‌کند. copilot.byok.enabled سوئیچ استفاده از کلید شخصی (bring-your-own-key) است که به مالک فضای کاری اجازه می‌دهد کلید ارائه‌دهنده مدل خود را در تنظیمات فضای کاری وارد کند. میزبانی شخصی (self-hosting) برنامه 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) نشان دهد. هر کد خروجی دیگری در job مهاجرت داده‌ها (migration)، همان موردی است که باید بررسی شود و لاگ آن، مرحله‌ای که متوقف شده را مشخص می‌کند:

docker compose logs affine_migration

تصویر را پیش از فراموشی ثابت (Pin) کنید

stable یک تگ متغیر است. گردش کار انتشار AFFiNE چندین تگ را به هر نسخه پایدار اشاره می‌دهد که دو مورد از آن‌ها در اینجا اهمیت دارند: stable که در هر انتشار به‌روزرسانی می‌شود، و stable- که به دنبال آن هش کوتاه git می‌آید و تغییر نمی‌کند. اگر روی stable باقی بمانید، یک docker compose pull در شش ماه آینده، تصویر متفاوتی را دریافت کرده و migrationهای آن را روی دیتابیس شما در زمانی که انتخاب نکرده‌اید، اجرا می‌کند. دقیقاً همان تصویری را که تست کرده‌اید، ثابت (Pin) کنید:

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

این دستور خطی مانند ghcr.io/toeverything/affine@sha256: به همراه یک هش طولانی را چاپ می‌کند. کل رشته را در خط image: در هر دو فایل affine و affine_migration کپی کنید. این دو فایل همیشه باید با هم مطابقت داشته باشند، زیرا آن‌ها یک تصویر واحد هستند که دو نقش را ایفا می‌کنند و عدم تطابق به معنای migration دیتابیس به یک طرح (schema) و سرویس‌دهی با طرحی دیگر است. در این صورت، ارتقا یک ویرایش آگاهانه خواهد بود نه یک غافلگیری: digest را تغییر دهید، نسخه پشتیبان تهیه کنید، سپس docker compose pull و docker compose up -d را اجرا کنید.

پیش از هر کس دیگری، حساب کاربری مدیر را ایجاد کنید

هنگامی که برای نخستین بار /admin را روی یک نمونه (instance) تازه باز می‌کنید، AFFiNE شما را به صفحه ایجاد حساب کاربری هدایت می‌کند، زیرا سرور هنوز هیچ مدیر (administrator) ندارد. در این فرآیند، هیچ کد دعوت یا توکن راه‌اندازی وجود ندارد. اولین شخصی که این صفحه را بارگذاری کند، مدیر سرور شما خواهد شد؛ بنابراین تا زمانی که ثبت‌نام نکرده‌اید، پورت باید بسته بماند.

به همین دلیل است که فایل 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 درون کانتینر mount شده است و تمام فایل‌های آپلودشده را در خود نگه می‌دارد.
  • ./config در مسیر /root/.affine/config mount شده است و config.json را در خود جای می‌دهد.

توسعه‌دهندگان در اینجا به‌جای استفاده از named volumes، از bind mounts استفاده کرده‌اند و این انتخاب آگاهانه است: شما می‌توانید این مسیرها را با دستورات معمولی tar کرده و کپی کنید، بدون اینکه نیاز باشد از Docker بپرسید آن‌ها را کجا قرار داده است. هزینه این کار این است که مالکیت فایل‌ها روی سیستم میزبان اکنون بر عهده شماست؛ موضوعی که در bind mounts and named volumes به آن پرداخته شده است.

نحوه پشتیبان‌گیری از AFFiNE

دو مورد نیاز به پشتیبان‌گیری دارند که روش پشتیبان‌گیری هر کدام متفاوت است. پایگاه داده یک سرور فعال است، بنابراین کپی کردن فایل‌های آن در حین اجرا منجر به ایجاد یک نسخه خراب می‌شود. در عوض از آن dump بگیرید:

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/

عملیات dump داخل کانتینر و از طریق سوکت محلی آن اجرا می‌شود، بنابراین نیازی به وارد کردن رمز عبور نیست. حجم فایل را در خروجی ls بررسی کنید. فایلی با حجم چند صد بایت به این معنی است که dump با شکست مواجه شده، اما shell فایل را ایجاد کرده است؛ این همان خطایی است که کاربران معمولاً 6 ماه بعد متوجه آن می‌شوند. استفاده از -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 تا تاریخ آگوست 2026 همچنان ذکر می‌کنند که قابلیت export تنظیمات از پنل مدیریت پیاده‌سازی نشده است، بنابراین فایل موجود روی دیسک تنها نسخه از تنظیمات شماست. هر سه فایل را از سرور خارج کنید. پشتیبانی که روی همان دیسکِ منبع اصلی قرار دارد، پشتیبان محسوب نمی‌شود.

بازیابی و یک تله در مراحل منتشرشده

پیش از آنکه به مراحل بازیابی نیاز پیدا کنید، آن‌ها را از مستندات رسمی بخوانید و با دقت بررسی کنید. طبق آنچه در اوت 2026 منتشر شده است، آن‌ها فایلی با نام affine.backup را به داخل container کپی کرده و سپس از روی ./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 استفاده می‌کند و این موضوع اختیاری نیست. مستندات در این باره صریح هستند: WebSocket پایه و اساس سیستم همگام‌سازی و همکاری AFFiNE است، بنابراین پروکسی که این اتصالات را ارتقا (upgrade) ندهد، فضای کاری ایجاد می‌کند که در آن ویرایش‌ها به‌طور بی‌صدا از همگام‌سازی باز می‌مانند. صفحه بارگذاری می‌شود، ورود به سیستم کار می‌کند، اما ویرایشی که در یک مرورگر انجام شده هرگز به مرورگر دیگر نمی‌رسد. در ابزارهای توسعه‌دهنده مرورگر خود، زبانه 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

نام certificate resolver باید با نام تعریف‌شده در پیکربندی Traefik شما مطابقت داشته باشد و loadbalancer.server.port پورت کانتینر یعنی 3010 است، نه پورت میزبان. Traefik اتصالات WebSocket را بدون نیاز به پیکربندی اضافی پروکسی می‌کند، بنابراین کار دیگری برای انجام دادن وجود ندارد. اگر بقیه پشته (stack) شما از قبل پشت Authentik برای احراز هویت یکپارچه قرار دارد، یک middleware از نوع forward auth روی این روتر دسترسی مرورگر به 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 را خودش مدیریت می‌کند.

آنچه در نسخه self-hosted نادیده گرفته شده است

پیش از انتقال یک تیم به این پلتفرم، با خود صادق باشید.

قابلیت همکاری بلادرنگ (real-time collaboration) وجود دارد و تمام توصیه‌های مربوط به تعیین ابعاد (sizing) بر اساس همین ویژگی است، چرا که مستندات خود AFFiNE مصرف حافظه را به سیستم همگام‌سازی و ادغام اسناد نسبت می‌دهد. ویرایش آفلاین دلیلی است که بسیاری از افراد به دنبال ابزارهای local-first هستند و اپلیکیشن دسکتاپ می‌تواند سرور self-hosted شما را به لیست workspaceهای خود اضافه کرده و به آن وارد شود. پیش از تصمیم‌گیری نهایی، رفتار دقیق آفلاین مورد نیاز تیم خود را تست کنید: در اپلیکیشن دسکتاپ و در حالی که شبکه قطع است ویرایش انجام دهید، دوباره متصل شوید و سپس نتیجه را در دستگاه دوم بررسی کنید. لیست ویژگی‌ها مدرک قطعی نیستند و این مورد نیز از این قاعده مستثنی نیست.

جستجوی متنی کامل (full-text search) سمت سرور در فایل compose پیش‌فرض غیرفعال است، جایی که AFFINE_INDEXER_ENABLED=false روی سرور و در job مهاجرت تنظیم شده است. فعال‌سازی آن به معنای افزودن یک container به نام Manticore Search است که پنجمین سرویس محسوب شده و حافظه بیشتری مصرف می‌کند. روی یک سرور 2 GB، این همان تغییری است که باعث عبور از محدودیت منابع می‌شود. جستجو در داخل کلاینت همچنان برای workspaceای که باز کرده‌اید کار می‌کند.

پیش از دعوت از افراد، دانستن دو محدودیت ضروری است. به یک workspace در نسخه self-hosted حداکثر 10 کاربر (seat) اختصاص داده می‌شود و عبور از این تعداد نیازمند تهیه لایسنس Team از AFFiNE است. فضای ذخیره‌سازی blob نامحدود و اندازه نامحدود برای blobها در نمونه‌های self-hosted، طبق مستندات، در برنامه قرار دارد اما تا اوت 2026 هنوز به‌طور کامل پیاده‌سازی نشده است. هیچ‌کدام از این موارد برای یک خانواده یا یک تیم کوچک اهمیتی ندارد. اما اگر قصد دارید چهل نفر را به این پلتفرم منتقل کنید، هر دو مورد اهمیت پیدا می‌کنند.

ارتقاها

ابتدا یادداشت‌های انتشار (release notes) را مطالعه کنید، به‌ویژه برای تغییرات جزئی نسخه مانند 0.26 به 0.27 که ممکن است شامل تغییرات ناسازگار (breaking changes) باشد. پیش از هر اقدامی، از پایگاه داده و دایرکتوری ذخیره‌سازی نسخه پشتیبان تهیه کنید؛ زیرا عملیات مهاجرت (migration) در شروع بعدی، طرح‌واره (schema) شما را تغییر می‌دهد و امکان بازگشت (undo) وجود ندارد. سپس digest ثابت‌شده را تغییر دهید، دستور docker compose pull و به دنبال آن docker compose up -d را اجرا کنید و خروجی docker compose logs -f affine_migration را تا زمانی که با موفقیت پایان یابد، نظارت کنید. دستور docker image prune پس از آن لایه‌های قدیمی را پاکسازی می‌کند. یک نکته تاریخی برای کسانی که از نسخه‌های بسیار قدیمی استفاده می‌کنند: از نسخه 0.23.0 نام image از affine-graphql به affine تغییر یافته است؛ بنابراین فایل‌های compose قدیمی‌تر از آن نسخه، پیش از آنکه عملیات pull بتواند محتوایی پیدا کند، نیاز به بازنویسی خطوط image دارند.

FAQ

چرا کانتینر AFFiNE هرگز اجرا نمی‌شود؟

سرویس affine بر روی job مربوط به affine_migration، وابستگی condition: service_completed_successfully را تعریف کرده است؛ بنابراین اگر عملیات migration با هر وضعیتی غیر از 0 خاتمه یابد، سرور هرگز اجرا نمی‌شود و رابط کاربری وب اصلاً ظاهر نخواهد شد. دستور docker compose logs affine_migration را اجرا کنید تا ببینید کدام مرحله متوقف شده است. رایج‌ترین علت در فایل‌های compose که به‌صورت دستی ویرایش شده‌اند، استفاده از ایمیج استاندارد postgres به‌جای pgvector/pgvector:pg16 است؛ زیرا طرح (schema) برنامه AFFiNE افزونه pgvector را فراخوانی کرده و جداولی با ستون‌های vector(1024) ایجاد می‌کند که Postgres معمولی قادر به ساخت آن‌ها نیست.

برنامه AFFiNE برای میزبانی شخصی به چه مقدار رم نیاز دارد؟

صفحه نیازمندی‌های AFFiNE حداقل 4 هسته CPU و 2 گیگابایت رم را پیشنهاد می‌دهد که با عبور تعداد کلمات اسناد از 10,000 کلمه، این مقدار به 4 گیگابایت افزایش می‌یابد. همچنین ذکر شده که ادغام یک سند با 10,000 تغییر می‌تواند تا 1 گیگابایت رم مصرف کند. در سروری با 2 گیگابایت رم، این اوج مصرف است که باعث مشکل می‌شود، نه بار کاری عادی؛ زیرا مکانیزم 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 گیگابایت اضافه کنید تا جهش‌های مصرف رم به‌جای ایجاد اختلال مرگبار، فقط باعث کندی شوند.

برنامه AFFiNE داده‌های مرا کجا ذخیره می‌کند و از چه چیزی باید نسخه پشتیبان تهیه کنم؟

سه مسیر در دایرکتوری compose شما همه چیز را در خود نگه می‌دارند: ./data/postgres برای پایگاه داده، ./data/storage برای فایل‌های آپلود شده و ./config برای config.json. برای پشتیبان‌گیری از پایگاه داده از docker compose exec -T postgres pg_dump --format c --username affine affine > affine.dump استفاده کنید و از کپی مستقیم فایل‌ها خودداری کنید، زیرا کپی کردن فایل‌های یک Postgres در حال اجرا ایمن نیست. برای فایل‌های آپلود شده از ./data/storage خروجی tar بگیرید و یک نسخه از config.json را به‌صورت دستی نگهداری کنید، چرا که طبق اطلاعات تا اوت 2026، قابلیت خروجی گرفتن از تنظیمات از طریق پنل مدیریت هنوز پیاده‌سازی نشده است.

آیا همکاری هم‌زمان (real-time collaboration) در نسخه خود-میزبانی AFFiNE کار می‌کند؟

بله، و برای فعال‌سازی آن نیازی به انجام کاری نیست. تنها پیش‌نیاز، تنظیمات reverse proxy شماست، زیرا همگام‌سازی از طریق اتصالات WebSocket انجام می‌شود. در nginx این به معنای proxy_http_version 1.1 به همراه هدرهای Upgrade و Connection: upgrade است، در حالی که Traefik و Caddy این اتصالات را بدون نیاز به تنظیمات اضافی عبور می‌دهند. نشانه پروکسی که این اتصالات را ارتقا (upgrade) نمی‌دهد این است که فضای کاری به‌طور عادی بارگذاری شده و ورود انجام می‌شود، اما ویرایش‌های انجام شده در یک مرورگر هرگز در مرورگر دیگر ظاهر نمی‌شوند.

آیا می‌توانم AFFiNE را با ایمیج استاندارد Postgres اجرا کنم؟

خیر. فایل schema.prisma برنامه AFFiNE، افزونه extensions = [pgvector(map: "vector")] را فراخوانی کرده و چهار جدول با ستون embedding از نوع vector(1024) تعریف می‌کند؛ و job مربوط به migration حتی زمانی که قابلیت‌های هوش مصنوعی خاموش باشند، این جداول را ایجاد می‌کند. از pgvector/pgvector:pg16 استفاده کنید که همان Postgres 16 با افزونه کامپایل‌شده است. اگر AFFiNE را به یک سرور Postgres خارجی متصل می‌کنید، قبل از اجرای migration، افزونه pgvector را روی آن نصب کرده و در پایگاه داده مقصد ایجاد کنید.