SSD Nodes Learn Hosting plans →
নির্দেশিকা Matt Connorদ্বারা Matt Connor

Docker Compose-এ AFFiNE self-host করার সম্পূর্ণ গাইড

একটি VPS-এ Docker Compose দিয়ে AFFiNE চালান: 4টি container, pinned image tag, data কোথায় থাকে, backup এবং 2 GB RAM-এ বাস্তবে কতটা ব্যবহার করা যায় জানুন।

আপনি AFFiNE self-host করলে যা পাবেন

AFFiNE self-host করলে আপনার নিয়ন্ত্রণাধীন একটি সার্ভারে Notion-এর মতো workspace পাওয়া যায়। এটি 4টি container হিসেবে চলে: application, one-shot migration job, Postgres এবং Redis। Real-time collaboration অন্তর্ভুক্ত থাকে। self-hosted workspace-এ ডিফল্টভাবে সর্বোচ্চ 10টি seat থাকে। ইনস্টল করতে একটি compose file এবং একটি JSON config file যথেষ্ট। তবে image tag, disk layout, memory ceiling এবং সামনে বসানো proxy নিয়ে পরিকল্পনা করতে হবে।

AFFiNE একই workspace-এ document editor এবং infinite canvas রাখে। তাই একটি page-কে document হিসেবে পড়া যায় অথবা whiteboard হিসেবে বিস্তৃত করা যায়। আপনি কোন software চালাবেন তা এখনও ঠিক না করলে আগে self-hosted Notion বিকল্পগুলোর তুলনা পড়ুন। এই guide-এ ধরে নেওয়া হয়েছে যে সিদ্ধান্ত নেওয়া হয়েছে। এখানে আবার তুলনা না করে AFFiNE সঠিকভাবে চালানোর পদ্ধতি দেখানো হয়েছে।

এই guide-এর সব তথ্য AFFiNE-এর self-host documentation এবং 8 August 2026-এ প্রকাশিত release file-এর সঙ্গে যাচাই করা হয়েছে। ওই তারিখে সর্বশেষ stable release ছিল 0.27.3, যা 23 July 2026-এ প্রকাশিত হয়। নিচের যেসব দাবির সঙ্গে পরবর্তী তারিখ দেওয়া আছে, সেগুলো 12 October 2026-এ primary source-এর সঙ্গে পুনরায় যাচাই করা হয়েছে। তখন সর্বশেষ stable release ছিল 0.27.4, যা 18 August 2026-এ প্রকাশিত হয়।

চারটি container আসলে কী করে

affine একই image-এ server এবং web client হিসেবে কাজ করে। এটি port 3010-এ listening করে।

affine_migration একটি one-shot job, যা node ./scripts/self-host-predeploy.js চালায়, database migration প্রয়োগ করে এবং তারপর বন্ধ হয়ে যায়। Application-টি ওই job-এর ওপর condition: service_completed_successfully ঘোষণা করে। তাই migration non-zero status দিয়ে বন্ধ হলে affine একেবারেই start হয় না। Web interface চালু না হলে প্রথমে ওই job-এর log পড়ুন।

postgres আপনার document, user, workspace এবং permission সংরক্ষণ করে। সরবরাহ করা image হলো pgvector/pgvector:pg16। এটি pgvector extension-সহ সাধারণ Postgres 16। pgvector Postgres-এ একটি vector column type যোগ করে। Text-এর embedding সংরক্ষণে এই numeric form ব্যবহার করা হয়, যাতে অর্থের ভিত্তিতে text search করা যায়।

redis একটি hard dependency। Server এবং migration job—উভয়ই start হওয়ার আগে এর health check-এর জন্য অপেক্ষা করে। সরবরাহ করা compose file Redis-কে যা দেয় না, তা হলো একটি volume। docker compose down-এর পর এর ভেতরের কোনো কিছু টিকে থাকে না। তাই স্পষ্ট যে এতে আপনার কোনো content থাকে না এবং এর backup-এর প্রয়োজন নেই।

কেন Postgres image pgvector, stock postgres নয়

এই প্রয়োজনীয়তা AFFiNE-এর schema থেকে এসেছে; এটি কোনো পছন্দের বিষয় নয়। v0.27.4-এ schema.prisma এখনও extensions = [pgvector(map: "vector")] ঘোষণা করে, কিন্তু এতে কোনো embedding column নেই। v0.27.3-এ vector(1024) টাইপের embedding column থাকা চারটি model v0.27.4-এ নেই। সরবরাহ করা pgvector/pgvector:pg16 image-ই ব্যবহার করুন: extension এখনও ঘোষণা করা আছে, আর stock postgres:16-এ এটি অন্তর্ভুক্ত নেই।

অফিশিয়াল v0.27.4 template-এ pgvector image ব্যবহার করা হয়েছে; v0.21.0 template-এ এখনও postgres:16 ব্যবহার করা হতো। পুরোনো install-এ image line সম্পাদনা করাই সম্পূর্ণ upgrade নয়। তাই কিছু pull করার আগে AFFiNE self-host docs-এর upgrade পৃষ্ঠা পড়ুন।

ওই tag সম্পর্কে আরও একটি বিষয় আছে। pg16 বলতে Postgres 16 বোঝায়, এবং Postgres major version এমন কোনো সংখ্যা নয় যা সরাসরি বাড়ানো যায়। বিদ্যমান data directory-তে এটিকে pg17 পরিবর্তন করলে Postgres চালু হতে অস্বীকৃতি জানায়। docker compose logs postgres-এ The data directory was initialized by PostgreSQL version 16, which is not compatible with this version 17-এর মতো একটি line দেখা যাবে। Major version পরিবর্তনের জন্য PostgreSQL 17 release notes-এ উল্লেখ করা তিনটি পদ্ধতির একটি ব্যবহার করতে হবে: dump নিয়ে নতুন data directory-তে restore করা, pg_upgrade, অথবা logical replication। একটি একক Compose install-এ dump ও restore সাধারণত সবচেয়ে সহজ পদ্ধতি, কারণ pg_upgrade-এর জন্য পুরোনো এবং নতুন server binary পাশাপাশি থাকা প্রয়োজন।

Self-hosted AFFiNE-এর জন্য কত CPU এবং RAM প্রয়োজন

AFFiNE-এর requirements page-এ কমপক্ষে 4 CPU core এবং 2 GB RAM চাওয়া হয়েছে। আপনার document 10,000 শব্দ ছাড়িয়ে গেলে প্রয়োজনীয় memory 4 GB-এ বেড়ে যায়। একই page-এ memory কোথায় ব্যবহৃত হয়, সেটিও বলা আছে: sync system এবং document merging। মনে রাখার মতো একটি পরিসংখ্যান হলো, 10,000টি modification থাকা একটি document merge করার সময় memory সর্বোচ্চ 1 GB পর্যন্ত উঠতে পারে।

এখন দুইজন ব্যক্তি লিখছেন—এমন 2 GB plan-এর সঙ্গে বিষয়টি মিলিয়ে দেখুন। গড় ব্যবহারে সমস্যা হয় না। Postgres এবং Node process সীমার নিচে থাকে, কিছু memory অবশিষ্টও থাকে। সমস্যা হয় peak usage-এ। একটি বড় merge server-এর memory usage প্রায় 1 GB পর্যন্ত তুলতে পারে। 2 GB-এর এমন একটি box-এ swap না থাকলে, এই peak-এর সঙ্গে Postgres এবং memory-তে থাকা অন্যান্য process যোগ হয়ে পুরো memory শেষ হয়ে যেতে পারে। তখন kernel-এর out-of-memory (OOM) killer memory খালি করতে একটি process বন্ধ করে দেয়। AFFiNE server সবচেয়ে বড় process হওয়ায় সেটিই বন্ধ হওয়ার সম্ভাবনা বেশি। তবে Docker-এর documentation স্পষ্টভাবে বলে যে Docker নিজেসহ যেকোনো process নির্বাচিত হতে পারে।

যখন AFFiNE server বন্ধ হয়ে যায়, আপনার সহকর্মী কোনো error দেখতে পান না। তারা শুধু page reload হতে দেখেন, কারণ restart: unless-stopped কয়েক সেকেন্ডের মধ্যে container আবার চালু করে। এটি অনুমান করবেন না; নিশ্চিত করুন:

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

প্রথম command-এর true অথবা দ্বিতীয় command-এ node উল্লেখ করা কোনো Killed process line-এর অর্থ হলো memory শেষ হয়ে গিয়েছিল; এটি কোনো bug শনাক্ত হওয়ার অর্থ নয়। দুই দিক থেকেই সমাধান করুন। প্রথমে swap যোগ করুন, যাতে OOM killer কাজ করার আগে হঠাৎ memory spike-এর জন্য 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 total দেখানো উচিত। Swap AFFiNE-কে দ্রুত করে না এবং এর উদ্দেশ্যও তা নয়। এটি হঠাৎ memory spike সামলানোর জন্য সাময়িক জায়গা দেয়। ফলে সেই spike-এর কারণে container বন্ধ হওয়ার বদলে সাময়িকভাবে ধীর হওয়ার সম্ভাবনাই বেশি থাকে। এটি একটি buffer, নিশ্চয়তা নয়। সমাধানের অন্য দিক হলো merge-এর সময় application-এর প্রয়োজনীয় memory যেন Postgres তার cache দিয়ে পূর্ণ না করে। Compose service-এ memory limit এই কাজের জন্যই ব্যবহৃত হয়। উভয় পরিবর্তনের পরও process বন্ধ হতে থাকলে, বড় document ব্যবহারের জন্য AFFiNE-এর requirements page-এ চাওয়া 4 GB RAM server-এ প্রয়োজন।

Storage-এর হিসাব অনুমান করা অনেক সহজ। একই page-এ 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
  }
]

Server install-এর জন্য 1.5 GB লাগে। প্রতিটি প্রায় এক হাজার শব্দের 1,000টি document Postgres data হিসেবে 0.1 GB যোগ করে, যা প্রায় কিছুই নয়। 1,000টি uploaded file 10 GB যোগ করে, এবং মূল storage প্রয়োজন মূলত এখানেই। এগুলো চালু থাকা instance থেকে নেওয়া measurement নয়; এগুলো প্রকাশিত planning figure। তাই এগুলোকে সুনির্দিষ্ট প্রতিশ্রুতি নয়, বরং ব্যবহারের ধরন বোঝার নির্দেশনা হিসেবে দেখুন। গুরুত্বপূর্ণ বিষয় হলো: আপনার database ছোট থাকবে, আর disk usage নির্ধারণ করবে আপনার upload।

নিজে Compose ফাইল লিখুন এবং image tag নির্দিষ্ট করে দিন

নথিভুক্ত ইনস্টল পদ্ধতিতে curl -L -o docker-compose.yml https://github.com/toeverything/AFFiNE/releases/latest/download/docker-compose.yml ব্যবহার করে একটি প্রস্তুত ফাইল ডাউনলোড করা হয়। এটি কাজ করে। তবে এর ওপর নির্ভর করার আগে একটি বিষয় জানা দরকার: 8 August 2026 অনুযায়ী release 0.27.3-এর সঙ্গে সংযুক্ত ফাইলটি .env ফাইল থেকে path পড়ত এবং ${UPLOAD_LOCATION}, ${CONFIG_LOCATION} ও ${DB_DATA_LOCATION} ব্যবহার করত। কিন্তু documentation-এর reference page-এ storage ও PostgreSQL data ./data-এর অধীনে রাখা, configuration ./config-এর অধীনে রাখা এবং কোনো .env-এর প্রয়োজন নেই—এমন নতুন layout দেখানো হয়েছিল। Release 0.27.4 সংযুক্ত ফাইলটিকে ওই layout-এ সরিয়ে নেয়, এবং 12 October 2026 অনুযায়ী দুটির মধ্যে আর পার্থক্য নেই। আপনি যদি পুরোনো ফাইল থেকে install করে থাকেন, আপনার path এখনও .env থেকে আসে। AFFiNE-এর upgrade page সতর্ক করে যে ওই settings সঙ্গে না নিয়ে compose file প্রতিস্থাপন করলে Postgres একটি খালি data directory নিয়ে চালু হতে পারে। নিজে ফাইল লিখলে এই অনিশ্চয়তা পুরোপুরি এড়ানো যায়। Image pin করা এবং database password সেট করার জন্য যেহেতু ফাইল সম্পাদনা করতেই হবে, তাই এই পদ্ধতিই ব্যবহার করুন।

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

Compose নিজে থেকেই project directory থেকে .env পড়ে এবং আপনার জন্য ${DB_PASSWORD} প্রতিস্থাপন করে। ফলে support thread-এ paste করার ফাইলে password দেখা যায় না। আপনি যে প্রতিটি stack চালান, সেখানেই এই অভ্যাস বজায় রাখুন। এর কারণ compose file-এ secret না রাখার পদ্ধতি-তে ব্যাখ্যা করা হয়েছে।

এখন ~/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 যে ফাইল সরবরাহ করে, তার সঙ্গে এখানে 4টি পার্থক্য আছে। প্রতিটিরই একটি কারণ রয়েছে।

  • 127.0.0.1:3010:3010 port-টি শুধু loopback address-এ publish করে। Docker 28.0.0-এর আগের release-গুলোতে একই L2 segment-এর host-গুলো localhost-published port-এ পৌঁছাতে পারে। Configured direct routing থাকলে loopback publishing সত্ত্বেও container port Internet-এর জন্য উন্মুক্ত হতে পারে। Docker 28.0.0 বা পরবর্তী version ব্যবহার করুন এবং setup চলাকালে direct routing বন্ধ রাখুন। Upstream-এর '3010:3010' প্রতিটি interface-এ bind করে। অধিকাংশ VPS image-এ এর মধ্যে public interface-ও থাকে।
  • POSTGRES_HOST_AUTH_METHOD: trust সরিয়ে password সেট করা হয়েছে। Trust authentication ব্যবহার করলে password ছাড়াই ওই database-এ করা যেকোনো connection affine user হিসেবে গ্রহণ করা হয়। এটি private Compose network-এ সীমাবদ্ধ থাকে। তবে debugging-এর সময় ওই network-এ আরেকটি container যুক্ত করলে বা 5432 publish করলে এই নিরাপত্তা আর থাকে না।
  • একটি সাধারণ redis-এর পরিবর্তে redis:8-alpine ব্যবহার করা হয়েছে। সাধারণ redis latest-এ resolve হয়। August 2026 অনুযায়ী সেটি Redis 8। তাই pin করলে পরীক্ষিত major version বজায় থাকে এবং কোনো সম্পর্কহীন docker compose pull-এর সময় ভবিষ্যতের Redis 9 এসে পড়া বন্ধ হয়।
  • pgvector/pgvector:pg16 upstream যেভাবে সেট করেছে, ঠিক সেভাবেই রাখা হয়েছে। এর কারণ উপরে দেওয়া হয়েছে।

Postgres প্রথমবার data directory তৈরি করার সময়ই POSTGRES_PASSWORD পড়ে। ইতিমধ্যে তৈরি instance-এ docker compose exec postgres psql -U affine -c "ALTER USER affine WITH PASSWORD 'yourpassword'" ব্যবহার করে password সেট করুন। এরপর password-এর সঙ্গে মিলিয়ে DATABASE_URL আপডেট করুন।

Configuration config/config.json ফাইলে থাকে

AFFiNE তার সেটিংস পড়ে config/config.json থেকে। আপনি এই directory-টি /root/.affine/config-এ mount করেছেন। কোনো কিছু স্বয়ংক্রিয়ভাবে এই file তৈরি করে না। তাই প্রথমবার start করার আগে এটি লিখুন। একটি editor-এ ~/affine/config/config.json খুলে নিচের content দিন। উদাহরণের domain-এর জায়গায় আপনার নিজের domain ব্যবহার করুন:

{
  "$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-এ এমন address দিতে হবে, যা আপনার ব্যবহারকারীরা browser-এ বাস্তবে খোলেন। AFFiNE এই value থেকে share link এবং workspace invitation তৈরি করে। তাই এটি http://localhost:3010-এ রেখে দিলে পাঠানো invitation প্রাপকের নিজের machine-এ নির্দেশ করবে এবং সেখানে কাজ করবে না। প্রথমবার start করার আগেই public HTTPS address সেট করুন। এতে file এবং admin panel-এ এই address নিয়ে অসঙ্গতি থাকবে না। পরে admin panel-এ পরিবর্তন করা settings database-এ সংরক্ষিত হয়; এই file-এ লেখা হয় না।

copilot AI feature নিয়ন্ত্রণ করে। copilot.byok.enabled হলো নিজের key ব্যবহারের switch। এটি workspace owner-কে workspace settings-এ নিজের model provider key paste করার অনুমতি দেয়। AFFiNE self-host করলেই কোনো AI subscription অন্তর্ভুক্ত হয় না। আপনি এটি ব্যবহার করতে না চাইলে উভয় false রাখুন।

Stack start করুন:

docker compose up -d
docker compose ps --all

docker compose ps --all-এ affine_postgres এবং affine_redis healthy, affine_server running, এবং affine_migration_job-এ state exited (0) দেখানো উচিত। --all গুরুত্বপূর্ণ। এটি ছাড়া docker compose ps শুধু running container দেখায়। ফলে সফলভাবে শেষ হওয়া migration job একেবারেই দেখা যায় না। Migration job-এ অন্য কোনো exit code দেখা গেলে সেটিই তদন্ত করতে হবে। এর log-এ কোন ধাপে কাজ থেমেছে তা উল্লেখ থাকে:

docker compose logs affine_migration

ইমেজটি ভুলে যাওয়ার আগেই নির্দিষ্ট করে দিন

stable একটি পরিবর্তনশীল tag। AFFiNE-এর release workflow প্রতিটি stable build-এর জন্য একাধিক tag নির্দেশ করে। এখানে দুটি tag গুরুত্বপূর্ণ: stable, যা প্রতিটি release-এ নতুন করে নির্দেশ করা হয়, এবং git short hash-সহ stable-, যা পরিবর্তন করা হয় না। stable ব্যবহার করে রাখলে, ছয় মাস পরে একটি docker compose pull ভিন্ন image fetch করবে এবং আপনার বেছে না নেওয়া সময়ে সেই image database-এর বিরুদ্ধে migration চালাবে। আপনি যে সঠিক image পরীক্ষা করেছেন, সেটি নির্দিষ্ট করে দিন:

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

এতে ghcr.io/toeverything/affine@sha256:-এর মতো একটি line দেখাবে, যার পরে একটি দীর্ঘ hash থাকবে। সম্পূর্ণ string-টি affine এবং affine_migration—উভয়ের image: line-এ বসান। এই দুটির মান সব সময় একই হতে হবে, কারণ একই image এখানে দুটি ভূমিকা পালন করছে। অমিল হলে database একটি schema-তে migrate হবে, কিন্তু service অন্য schema দিয়ে তা পরিবেশন করবে। এরপর upgrade একটি ইচ্ছাকৃত সম্পাদনা হবে, আকস্মিক ঘটনা নয়: digest পরিবর্তন করুন, backup নিন, docker compose pull, docker compose up -d।

অন্য কেউ করার আগে admin account তৈরি করুন

একটি নতুন instance-এ /admin খুললে AFFiNE আপনাকে account তৈরির পৃষ্ঠায় পাঠায়, কারণ সার্ভারে এখনো কোনো administrator নেই। এই workflow-এ কোনো invitation code বা setup token নেই। registration সম্পন্ন করা প্রথম ব্যক্তি আপনার সার্ভারের administrator হয়ে যান। তাই আপনি registration সম্পন্ন না করা পর্যন্ত port বন্ধ রাখতে হবে।

এই কারণেই উপরের compose file-এ 127.0.0.1-এ bind করা হয়েছে। নিজের machine থেকে SSH tunnel ব্যবহার করে এতে প্রবেশ করুন:

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

এই tunnel চালু রাখুন এবং নিজের local browser-এ http://127.0.0.1:3010/admin খুলুন। registration ও login সম্পন্ন করুন, তারপর tunnel বন্ধ করুন। কেবল এরপরই instance-টি public name-এ প্রকাশ করা নিরাপদ। অন্যান্য self-hosted app-এও একই race condition দেখা যায়। প্রথম login-এর সময় hostname-এর সঙ্গে যুক্ত একটি passkey তৈরি হলে ঝুঁকি আরও বাড়ে। তাই আপনি যখন self-host openGym করবেন, তখন প্রথম account তৈরির আগেই TLS এবং final domain নির্ধারণ করতে হবে।

AFFiNE আপনার ডেটা যেখানে রাখে

তিনটি path-এ সবকিছু সংরক্ষিত থাকে, এবং তিনটিই আপনার তৈরি করা directory-এর ভেতরে থাকে।

  • ./data/postgres হলো Postgres data directory। এখানে documents, users, workspaces, permissions এবং admin panel থেকে সংরক্ষিত server settings থাকে।
  • ./data/storage container-এর ভেতরে /root/.affine/storage-এ mount করা থাকে এবং এখানে upload করা সব file থাকে।
  • ./config /root/.affine/config-এ mount করা থাকে এবং এখানে migration job দ্বারা তৈরি config.json ও private.key থাকে।

এখানে upstream named volume-এর পরিবর্তে bind mount ব্যবহার করে, এবং এটি ইচ্ছাকৃত সিদ্ধান্ত। আপনি সাধারণ command ব্যবহার করে এই path-গুলো tar করে copy করতে পারেন; Docker এগুলো কোথায় রেখেছে তা জানতে হয় না। এর বিনিময়ে host-এর file ownership এখন আপনাকেই পরিচালনা করতে হবে। এই trade-off-টি bind mount এবং named volume-এ ব্যাখ্যা করা হয়েছে।

AFFiNE-এর ব্যাকআপ কীভাবে নেবেন

দুটি জিনিসের ব্যাকআপ নিতে হবে, এবং সেগুলোর পদ্ধতি আলাদা। Database একটি চলমান server, তাই server চালু থাকা অবস্থায় এর file copy করলে সেটি corrupt হতে পারে। পরিবর্তে 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 container-এর ভেতরে তার local socket ব্যবহার করে চলে, তাই password চায় না। ls output-এ file-এর size পরীক্ষা করুন। কয়েকশো byte-এর file হলে বুঝবেন dump ব্যর্থ হয়েছে, কিন্তু shell তবুও file তৈরি করেছে। ছয় মাস পরে মানুষ সাধারণত এই ব্যর্থতা আবিষ্কার করে। -T-ও গুরুত্বপূর্ণ: এটি না থাকলে Compose একটি terminal বরাদ্দ করতে পারে এবং binary stream corrupt করতে পারে।

Uploaded file-গুলো সাধারণ file, তাই সেগুলো tar করুন:

tar czf backup/storage-$(date +%F).tgz -C data storage
tar czf backup/config-$(date +%F).tgz config docker-compose.yml .env

সম্পূর্ণ config directory, Compose file এবং .env backup-এ রাখুন, কারণ dump-এর মধ্যে এগুলো থাকে না। Migration job-টি config/private.key-ও তৈরি করে, তাই শুধু config.json copy করলে ওই file বাদ পড়বে। Admin panel-এ সংরক্ষিত settings database-এ থাকে, তাই উপরের dump-এই সেগুলো অন্তর্ভুক্ত থাকে। এছাড়া 12 October 2026 অনুযায়ী AFFiNE-এর backup page জানায় যে পরিবর্তিত server settings-ও admin panel থেকে export করা যায়। তিনটি file-ই server-এর বাইরে copy করুন। যে disk-এ সুরক্ষিত জিনিসটি আছে, সেই একই disk-এ রাখা backup প্রকৃত backup নয়। Database dump এবং uploads directory-এর tar—এই বিভাজনটি আপনি চালানো প্রতিটি stateful container-এর ক্ষেত্রেই অনুসরণ করুন। Support desk হিসেবে নিজে Chatwoot host করলেও conversation history এবং attachment নিরাপদ রাখার জন্য একই পদ্ধতি প্রযোজ্য।

পুনরুদ্ধার এবং প্রকাশিত ধাপগুলোর একটি ফাঁদ

প্রয়োজন হওয়ার আগেই অফিসিয়াল পুনরুদ্ধার-ধাপ পড়ুন এবং মনোযোগ দিয়ে পড়ুন। 2026 সালের August-এ প্রকাশিত নির্দেশনায় affine.backup নামের একটি ফাইল container-এ কপি করে পরে ./pg.backup থেকে পুনরুদ্ধার করতে বলা হয়েছিল। এগুলো দুটি ভিন্ন নাম। একই নির্দেশনায় ./postgres directory মুছে ফেলতে বলা হয়েছিল, অথচ বর্তমান compose file তার data ./data/postgres-এ রাখে। 12 October 2026 অনুযায়ী পৃষ্ঠাটিতে সর্বত্র affine.backup ব্যবহার করা হয়েছে এবং পুনরুদ্ধার যাচাই না হওয়া পর্যন্ত পুরোনো Postgres directory রেখে দিতে বলা হয়েছে। তাই upstream-এ সমস্যাটি ঠিক করা হয়েছে। তবে তার আগে সংরক্ষণ করা নির্দেশনার কপিতে এখনও এই ফাঁদ থাকতে পারে। যেকোনো ক্ষেত্রে, snippet-এ থাকা path নয়, আপনি বাস্তবে যে path ব্যবহার করেছেন সেটিই অনুসরণ করুন। এই guide-এর layout অনুযায়ী ক্রমটি হলো:

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

rm-এর বদলে mv ব্যবহারের বিষয়টি লক্ষ্য করুন। যে database-এর কপি আপনি রাখেননি, তার ওপর পুনরুদ্ধার করলে একটি ভুল command সম্পূর্ণ data loss ঘটাতে পারে। পুরোনো directory অন্যত্র সরিয়ে রাখতে কোনো খরচ হয় না। tar xzf backup/storage-2026-08-08.tgz -C data দিয়ে uploads-ও পুনরুদ্ধার করুন। তা না হলে প্রতিটি document-এর attachment ভাঙা অবস্থায় প্রদর্শিত হবে। এরপর লগ ইন করে image-যুক্ত একটি document খুলুন। এটিই পরীক্ষা। Browser-এ না খোলা restore একটি file, backup নয়।

ইতিমধ্যে চালানো proxy-এর পেছনে AFFiNE স্থাপন করা

AFFiNE WebSocket ব্যবহার করে, এবং এটি ঐচ্ছিক নয়। documentation-এ বিষয়টি স্পষ্টভাবে বলা আছে: WebSocket হলো AFFiNE-এর sync ও collaboration system-এর ভিত্তি। তাই কোনো proxy এই connection-গুলো upgrade না করলে এমন workspace তৈরি হয় যেখানে editing নীরবে sync হওয়া বন্ধ করে দেয়। Page load হয়, login কাজ করে, কিন্তু একটি browser-এ করা edit অন্য browser-এ পৌঁছায় না। আপনার browser-এর developer tools-এ Network tab খুলে WS filter করুন। যে connection বারবার open ও close হয়, সেটি সাধারণত এমন proxy নির্দেশ করে যা upgrade pass করছে না।

আপনি যদি অন্য container-এর জন্য আগে থেকেই Traefik চালান, তাহলে AFFiNE-কে একটি সাধারণ service হিসেবে এতে যুক্ত করুন। affine service থেকে ports: block মুছে দিন, তারপর যোগ করুন:

    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'

এবং file-এর নিচে, services:-এর পাশে যোগ করুন:

networks:
  proxy:
    external: true

Certificate resolver-এর নামটি আপনার Traefik configuration-এ সংজ্ঞায়িত নামের সঙ্গে মিলতে হবে। loadbalancer.server.port হলো container port 3010; এটি কখনোই host port নয়। Traefik অতিরিক্ত configuration ছাড়াই WebSocket connection proxy করে, তাই আর কিছু যোগ করার প্রয়োজন নেই। আপনার stack-এর বাকি অংশ যদি ইতিমধ্যে single sign-on-এর জন্য Authentik-এর পেছনে থাকে, তাহলে এই router-এ একটি forward auth middleware ব্যবহার করে AFFiNE-তে browser access নিয়ন্ত্রণ করা যাবে। তবে desktop app পরীক্ষা না করা পর্যন্ত এটি সক্রিয় রাখবেন না। Desktop app কোনো browser session বহন করে না এবং তখন sync ব্যর্থ হবে। একই instance-এর পেছনে একাধিক app চালানোর বিষয়টি একাধিক app-এর সামনে একটি Traefik-এ ব্যাখ্যা করা হয়েছে।

nginx-এ upgrade স্পষ্টভাবে নির্ধারণ করতে হয়:

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;
}

nginx-এ client_max_body_size-এর default মান 1 MB। তাই ওই line না থাকলে ছোট photo-এর চেয়ে বড় প্রতিটি upload 413 status-এ ব্যর্থ হয়। AFFiNE log-এ কিছু দেখা যায় না, কারণ request-টি সেখানে পৌঁছায়ইনি। Caddy-এর জন্য একটি line যথেষ্ট, reverse_proxy http://127.0.0.1:3010। এটি নিজেই certificate এবং WebSocket upgrade পরিচালনা করে।

self-hosted build-এ যা থাকে না

কোনো team-কে এতে স্থানান্তর করার আগে এই সীমাবদ্ধতাগুলো সম্পর্কে নিজের কাছে সৎ থাকুন।

Real-time collaboration আছে। Sizing-সংক্রান্ত পরামর্শের মূল বিষয়ও এটি, কারণ AFFiNE-এর নিজস্ব documentation অনুযায়ী memory ব্যবহার sync system এবং document merging-এর জন্য হয়। অনেকেই local-first tool চান offline editing-এর জন্য। Desktop application আপনার self-hosted server-কে workspace list-এ যোগ করতে পারে এবং সেটিতে login করতে পারে। সিদ্ধান্ত নেওয়ার আগে আপনার team যে নির্দিষ্ট offline আচরণের ওপর নির্ভর করে, তা পরীক্ষা করুন: network বন্ধ রেখে desktop app-এ edit করুন, আবার connect করুন, তারপর দ্বিতীয় device-এ ফলাফল পরীক্ষা করুন। Feature list প্রমাণ নয়। এই তালিকাটিও তার ব্যতিক্রম নয়।

Shipped compose file-এ server-side full-text search বন্ধ থাকে। সেখানে AFFINE_INDEXER_ENABLED=false server এবং migration job—উভয় জায়গাতেই সেট করা আছে। এটি চালু করতে একটি Manticore Search container যোগ করতে হয়। এতে পঞ্চম service এবং আরও memory লাগে। 2 GB-এর machine-এ এই পরিবর্তনই আপনাকে সীমার বাইরে ঠেলে দিতে পারে। Client-এর ভেতরের search খোলা workspace-এ এখনও কাজ করে।

মানুষকে আমন্ত্রণ জানানোর আগে দুটি সীমা জানা দরকার। একটি self-hosted workspace-এ সর্বোচ্চ 10টি seat দেওয়া যায়। এর বেশি হলে AFFiNE-এর Team license প্রয়োজন। Self-hosted instance-এর জন্য unlimited blob storage এবং unlimited blob size documentation-এ প্রত্যাশিত feature হিসেবে বর্ণিত হয়েছে, তবে October 2026-এ এগুলো এখনও সম্পূর্ণভাবে implemented হয়নি। Household বা ছোট team-এর ক্ষেত্রে এর কোনোটিই গুরুত্বপূর্ণ নয়। তবে আপনি যদি 40 জনকে স্থানান্তরের পরিকল্পনা করে থাকেন, দুটিই গুরুত্বপূর্ণ।

আপগ্রেড

প্রথমে release notes পড়ুন, বিশেষ করে 0.26 থেকে 0.27-এর মতো minor version bump-এর ক্ষেত্রে, যেখানে breaking change থাকতে পারে। কোনো পরিবর্তন করার আগে database এবং storage directory-এর backup নিন, কারণ পরবর্তী start-এর সময় migration job আপনার schema পরিবর্তন করবে এবং এটি পূর্বাবস্থায় ফেরানোর কোনো উপায় নেই। এরপর pinned digest পরিবর্তন করুন, docker compose pull চালিয়ে তার পরে docker compose up -d চালান, এবং পরিষ্কারভাবে শেষ না হওয়া পর্যন্ত docker compose logs -f affine_migration monitor করুন। পরে docker image prune চালিয়ে পুরোনো layer-গুলো সরিয়ে দিন। খুব পুরোনো install ব্যবহারকারীদের জন্য একটি ঐতিহাসিক তথ্য: version 0.23.0 থেকে image name affine-graphql থেকে affine-এ পরিবর্তিত হয়েছে। তাই এর চেয়ে পুরোনো compose file-এ pull সফল হওয়ার আগে image line-গুলো পুনর্লিখতে হবে। এছাড়া version 0.27.4 থেকে official compose template-এ Postgres, upload এবং configuration path পরিবর্তিত হয়েছে এবং default database connection-ও বদলেছে। তাই চালু install-এর ওপর নতুন file কপি না করে নতুন file-টি আপনার file-এর সঙ্গে তুলনা করুন।

FAQ

AFFiNE container কেন কখনো চালু হয় না?

affine service-এ affine_migration job-এর ওপর condition: service_completed_successfully নির্ভরতা ঘোষিত আছে। তাই migration 0 ছাড়া অন্য কোনো status নিয়ে শেষ হলে server আর চালু হয় না এবং কোনো web interface-ও দেখা যায় না। কোন ধাপে প্রক্রিয়া থেমেছে তা দেখতে docker compose logs affine_migration চালান। হাতে সম্পাদনা করা compose file-এ এখনও pgvector/pgvector:pg16 ব্যবহার করা হয়েছে কি না যাচাই করুন। Stock Postgres-এ pgvector extension থাকে না, অথচ AFFiNE-এর schema-তে এটি এখনও ঘোষিত আছে। v0.27.4-এ v0.27.3-এ থাকা চারটি embedding model আর সংজ্ঞায়িত করা হয় না।

Self-hosted AFFiNE-এর কত RAM প্রয়োজন?

AFFiNE-এর requirements page-এ অন্তত 4 CPU core এবং 2 GB RAM চাওয়া হয়েছে। Document 10,000 শব্দ ছাড়ালে প্রয়োজন 4 GB পর্যন্ত বাড়ে। সেখানে আরও বলা হয়েছে, 10,000টি modification থাকা একটি document merge করার সময় RAM ব্যবহার 1 GB পর্যন্ত উঠতে পারে। 2 GB server-এ সমস্যা idle load নয়, ওই peak ব্যবহার। Kernel-এর out-of-memory killer একটি process বন্ধ করার জন্য নির্বাচন করে। সবচেয়ে বড় process হওয়ায় সেটি সম্ভবত AFFiNE server-ই হয়। এরপর restart: unless-stopped সেটিকে আবার চালু করে। তাই ব্যবহারকারীরা error-এর বদলে page reload দেখতে পান। docker inspect affine_server --format '{{.State.OOMKilled}}' এবং sudo dmesg -T | grep -i 'out of memory' দিয়ে বিষয়টি নিশ্চিত করুন। স্বল্পমেয়াদি spike সামলানোর জন্য 2 GB-এর একটি swap file যোগ করুন। এই process বন্ধ হওয়া চলতে থাকলে 4 GB RAM-এ উন্নীত করুন।

AFFiNE আমার data কোথায় সংরক্ষণ করে এবং কোন data-এর backup নেব?

আপনার compose directory-এর অধীনে তিনটি path-এ সব data থাকে: database-এর জন্য ./data/postgres, uploaded file-এর জন্য ./data/storage, এবং config.json ও তৈরি করা private.key-এর জন্য ./config। File কপি না করে docker compose exec -T postgres pg_dump --format c --username affine affine > affine.dump দিয়ে database-এর backup নিন, কারণ চলমান Postgres নিরাপদে কপি করা যায় না। ওই dump-এ admin panel-এ সংরক্ষিত settings-ও থাকে, কারণ AFFiNE সেগুলো database-এ রাখে। Upload-এর জন্য ./data/storage-কে Tar করুন। পুরো config directory, Compose file এবং .env সংরক্ষণ করুন, কারণ এই file-গুলো database-এর অংশ নয়।

Self-hosted AFFiNE-এ কি real-time collaboration কাজ করে?

হ্যাঁ, এর জন্য কিছু enable করতে হয় না। একমাত্র প্রয়োজন reverse proxy, কারণ sync WebSocket connection-এর মাধ্যমে চলে। nginx-এ এর অর্থ proxy_http_version 1.1-এর সঙ্গে Upgrade এবং Connection: upgrade header ব্যবহার করা। Traefik এবং Caddy কোনো অতিরিক্ত configuration ছাড়াই এই connection pass করে। Proxy WebSocket connection upgrade না করলে workspace স্বাভাবিকভাবে load হয় এবং login করা যায়, কিন্তু একটি browser-এ করা edit অন্য browser-এ দেখা যায় না।

Stock Postgres image দিয়ে কি AFFiNE চালানো যায়?

pgvector/pgvector:pg16 ব্যবহার করুন। এতে pgvector extension-সহ Postgres 16 রয়েছে। AFFiNE v0.27.4 এখনও schema.prisma-এ extensions = [pgvector(map: "vector")] ঘোষণা করে, কিন্তু v0.27.3-এ থাকা vector(1024) embedding column-সহ চারটি model আর সংজ্ঞায়িত করে না। Stock Postgres-এ ঘোষিত extension থাকে না। এর পরিবর্তে AFFiNE-কে কোনো external Postgres server-এ সংযুক্ত করলে সেখানে pgvector install করুন এবং migration চালানোর আগে target database-এ extension তৈরি করুন।

এই লেখাটি কি কাজে লেগেছে?