SSD Nodes Learn Hosting plans →
Hướng dẫn Matt ConnorBởi Matt Connor · Cập nhật ngày 2026-08-27

Tự host AFFiNE bằng Docker Compose trên VPS

Chạy AFFiNE trên một VPS với 4 container, pin image tag 0.27.3, biết dữ liệu nằm ở đâu, cách backup và RAM 2 GB thực tế đáp ứng đến đâu.

Bạn nhận được gì khi tự host AFFiNE

Tự host AFFiNE cung cấp cho bạn một workspace kiểu Notion trên server do bạn kiểm soát. AFFiNE chạy dưới dạng 4 container: application, một migration job chạy một lần, Postgres và Redis. Tính năng cộng tác theo thời gian thực được tích hợp sẵn. Workspace tự host mặc định hỗ trợ tối đa 10 seat. Cài đặt chỉ cần một file compose và một file cấu hình JSON. Những điểm cần cân nhắc là image tag, cách bố trí disk, giới hạn memory và proxy đặt phía trước.

AFFiNE tích hợp document editor và infinite canvas trong cùng một workspace. Vì vậy, một page có thể được đọc như document hoặc mở rộng thành whiteboard. Nếu bạn vẫn đang chọn ứng dụng cần chạy, hãy đọc bài so sánh các lựa chọn thay thế Notion tự host trước. Hướng dẫn này giả định bạn đã chọn AFFiNE và tập trung vào cách vận hành AFFiNE đúng cách, không tiếp tục so sánh các lựa chọn.

Mọi nội dung trong đây đã được kiểm tra theo tài liệu self-host của AFFiNE và các release file được công bố vào ngày 8 August 2026. Stable release mới nhất vào ngày đó là 0.27.3, được phát hành ngày 23 July 2026.

Bốn container thực sự làm gì

affine chứa server và web client trong cùng một image. Nó lắng nghe trên port 3010.

affine_migration là một one-shot job chạy node ./scripts/self-host-predeploy.js, áp dụng các database migration rồi thoát. Application khai báo condition: service_completed_successfully đối với job đó, nên nếu migration thoát với status khác 0 thì affine hoàn toàn không khởi động. Khi web interface không hiển thị, log của job này là thứ cần đọc đầu tiên.

postgres lưu documents, users, workspaces và permissions của bạn. Image được cung cấp là pgvector/pgvector:pg16, tức Postgres 16 thông thường có biên dịch sẵn extension pgvector. pgvector thêm kiểu cột vector vào Postgres. Đây là dạng số dùng để lưu embeddings, nhờ đó có thể tìm kiếm text theo ý nghĩa.

redis là dependency bắt buộc: cả server và migration job đều chờ health check của nó trước khi khởi động. Lưu ý rằng file compose được cung cấp không cấp cho Redis một volume. Không có dữ liệu nào bên trong nó tồn tại sau docker compose down, nên rõ ràng nó không chứa content của bạn và không cần backup.

Vì sao image Postgres là pgvector thay vì postgres chuẩn

Yêu cầu này xuất phát từ schema của AFFiNE, không phải do sở thích. Trong schema.prisma, datasource khai báo extensions = [pgvector(map: "vector")], và bốn bảng có một cột embedding với kiểu vector(1024). Migration job sẽ tạo các bảng này dù bạn có bật tính năng AI hay không, nên extension phải tồn tại trong database trước khi migration hoàn tất. Thay postgres:16 vào thì extension biến mất, migration không thể tạo các cột đó, và server cứ chờ một job đã fail.

AFFiNE chuyển sang image pgvector từ version 0.21. Nếu bản cài đặt của bạn cũ hơn phiên bản đó, chỉ sửa dòng image là chưa đủ để upgrade. Hãy đọc trang upgrade trong tài liệu self-host của AFFiNE trước khi pull image.

Còn một điểm về tag đó. pg16 nghĩa là Postgres 16, và major version của Postgres không phải con số có thể tùy ý tăng. Nếu đổi sang pg17 trên một data directory hiện có, Postgres sẽ từ chối khởi động, với một dòng như The data directory was initialized by PostgreSQL version 16, which is not compatible with this version 17 trong docker compose logs postgres. Chuyển sang major version mới cần dump dữ liệu rồi restore vào một data directory mới.

AFFiNE cần bao nhiêu CPU và RAM khi tự host

Trang yêu cầu của AFFiNE yêu cầu ít nhất 4 CPU core và 2 GB RAM, đồng thời nâng mức bộ nhớ lên 4 GB khi tài liệu vượt quá 10,000 từ. Trang này cũng cho biết bộ nhớ được dùng vào đâu: hệ thống đồng bộ và việc gộp tài liệu. Có một con số đáng nhớ: việc gộp một tài liệu có 10,000 thay đổi có thể dùng tối đa 1 GB.

Bây giờ hãy đối chiếu con số đó với một plan 2 GB có 2 người cùng viết. Mức dùng trung bình không có vấn đề. Postgres và tiến trình Node vẫn nằm dưới giới hạn, còn dư bộ nhớ. Vấn đề là mức đỉnh. Một lần gộp lớn có thể cần thêm 1 GB ngoài toàn bộ bộ nhớ đang được sử dụng. Trên máy 2 GB không có swap, OOM killer của kernel sẽ đáp ứng yêu cầu đó bằng cách kill tiến trình lớn nhất, tức server AFFiNE.

Đồng nghiệp của bạn không thấy lỗi. Họ chỉ thấy trang reload vì restart: unless-stopped đưa container hoạt động trở lại trong vài giây. Đừng đoán. Hãy xác nhận:

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

true trong output của lệnh đầu tiên, hoặc một dòng Killed process ghi rõ node trong output của lệnh thứ hai, nghĩa là máy đã hết bộ nhớ chứ không phải AFFiNE có bug. Hãy xử lý từ cả 2 phía. Trước tiên, thêm swap để mức tăng đột biến trở nên chậm thay vì làm service chết:

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 bây giờ phải báo tổng swap là 2.0Gi. Swap không làm AFFiNE chạy nhanh hơn và cũng không được thiết kế cho mục đích đó. Nó biến một mức tăng đột biến kéo dài 1 giây thành một khoảng chậm thay vì làm container chết. Phía còn lại của cách xử lý là ngăn Postgres mở rộng cache vào phần bộ nhớ mà ứng dụng cần khi gộp tài liệu. Đây là mục đích của giới hạn bộ nhớ trên một service Compose.

Dung lượng lưu trữ dễ dự đoán hơn nhiều. Đây là các con số AFFiNE công bố trên cùng trang đó:

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

Bản cài đặt server dùng 1.5 GB. 1,000 tài liệu, mỗi tài liệu khoảng 1,000 từ, chỉ thêm 0.1 GB dữ liệu Postgres, gần như không đáng kể. 1,000 file được upload thêm 10 GB, và đó mới là phần chiếm dung lượng chính. Đây là các con số dùng để lập kế hoạch do AFFiNE công bố, không phải số đo từ một instance đang chạy, nên hãy xem chúng là xu hướng chứ không phải cam kết. Điều quan trọng là xu hướng: database của bạn vẫn nhỏ, còn file upload mới quyết định dung lượng disk.

Tự viết file Compose và pin tag

Tài liệu cài đặt tải file có sẵn bằng curl -L -o docker-compose.yml https://github.com/toeverything/AFFiNE/releases/latest/download/docker-compose.yml. Cách này hoạt động. Tuy nhiên, bạn nên biết một chi tiết trước khi phụ thuộc vào file đó: tính đến ngày 8 tháng 8 năm 2026, file đi kèm release 0.27.3 vẫn đọc các path từ file .env bằng ${UPLOAD_LOCATION}, ${CONFIG_LOCATION}${DB_DATA_LOCATION}, trong khi trang tham chiếu của tài liệu hiển thị layout mới hơn, giữ mọi thứ trong ./data và không cần .env. Cả hai đều là cách triển khai hợp lệ. Tự viết file sẽ giải quyết vấn đề này. Bạn cũng phải chỉnh sửa file để pin image và đặt password cho database.

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

Compose tự đọc .env trong project directory và thay ${DB_PASSWORD} cho bạn. Vì vậy, password không xuất hiện trong file mà bạn có thể dán vào một support thread. Nên duy trì thói quen này với mọi stack bạn chạy. Lý do được giải thích trong giữ secret ngoài file Compose.

Bây giờ hãy viết ~/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

File này khác file upstream cung cấp ở 4 điểm. Mỗi điểm đều có lý do.

  • 127.0.0.1:3010:3010 chỉ publish port trên địa chỉ loopback. Vì vậy, không có gì bên ngoài server có thể truy cập AFFiNE cho đến khi bạn quyết định cách expose dịch vụ. File upstream dùng '3010:3010' để bind trên mọi interface. Trên hầu hết VPS image, điều đó bao gồm cả interface public.
  • POSTGRES_HOST_AUTH_METHOD: trust được bỏ và thay bằng password. Cơ chế trust authentication chấp nhận mọi connection đến database đó dưới user affine mà không cần password. Cách này chỉ giới hạn trong private Compose network. Điều đó vẫn ổn cho đến khi bạn attach thêm một container vào network đó hoặc publish port 5432 để debug.
  • redis:8-alpine thay cho redis không có version cụ thể, vốn resolve thành latest. Tính đến tháng 8 năm 2026, đó là Redis 8. Việc pin version giữ nguyên major version bạn đã test và ngăn Redis 9 được cài trong một lần docker compose pull không liên quan.
  • pgvector/pgvector:pg16 được giữ nguyên như upstream đã đặt, vì lý do đã nêu ở trên.

POSTGRES_PASSWORD chỉ được đọc khi Postgres tạo data directory lần đầu. Trên instance đã tồn tại, hãy đặt password bằng docker compose exec postgres psql -U affine -c "ALTER USER affine WITH PASSWORD 'yourpassword'", sau đó cập nhật DATABASE_URL cho khớp.

Cấu hình nằm trong config/config.json

AFFiNE đọc các thiết lập từ config/config.json. Đây là thư mục bạn đã mount vào /root/.affine/config. Không có thành phần nào tự tạo file này, vì vậy hãy tạo file trước lần start đầu tiên. Mở ~/affine/config/config.json bằng editor và ghi nội dung sau, thay domain ví dụ bằng domain của bạn:

{
  "$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 phải là địa chỉ mà người dùng thực sự mở trong trình duyệt. AFFiNE dùng giá trị này để tạo share link và lời mời vào workspace. Nếu để là http://localhost:3010, lời mời bạn gửi sẽ trỏ người nhận đến chính máy của họ và bị lỗi. Hãy đặt giá trị này thành địa chỉ HTTPS public trước lần start đầu tiên, để file cấu hình và admin panel luôn thống nhất.

copilot điều khiển các tính năng AI. copilot.byok.enabled là tùy chọn cho phép dùng key riêng, để chủ sở hữu workspace nhập key của nhà cung cấp model vào phần cài đặt workspace. Việc self-host AFFiNE không bao gồm subscription AI. Nếu không cần tính năng này, hãy để trống cả hai false.

Start stack:

docker compose up -d
docker compose ps

docker compose ps phải liệt kê affine_postgresaffine_redis ở trạng thái healthy, affine_server ở trạng thái running, và affine_migration_job có state là exited (0). Bất kỳ exit code nào khác trên migration job đều cần được kiểm tra. Log của job sẽ cho biết bước nào đã dừng:

docker compose logs affine_migration

Ghim image trước khi quên

stable là một tag thay đổi theo thời gian. Quy trình release của AFFiNE gắn nhiều tag vào mỗi bản build ổn định. Trong đó có 2 tag cần lưu ý ở đây: stable, được gắn lại sau mỗi release, và stable- kèm theo git short hash, không bị thay đổi. Nếu vẫn dùng stable, lệnh docker compose pull sau 6 tháng sẽ tải một image khác và chạy migration trên database vào thời điểm bạn không chủ động chọn. Hãy pin đúng image đã kiểm thử:

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

Lệnh này in ra một dòng như ghcr.io/toeverything/affine@sha256: kèm theo một hash dài. Dán toàn bộ chuỗi đó vào dòng image: của cả affineaffine_migration. Hai giá trị này luôn phải giống nhau vì chúng là cùng một image nhưng đảm nhiệm 2 vai trò. Nếu không khớp, database có thể được migration sang một schema nhưng lại được phục vụ bằng schema khác. Khi đó, việc upgrade trở thành một thay đổi có chủ đích thay vì một sự cố bất ngờ: thay digest, backup, docker compose pull, docker compose up -d.

Tạo tài khoản admin trước khi người khác làm việc đó

Mở /admin trên một instance mới cài đặt. AFFiNE sẽ chuyển bạn đến trang tạo tài khoản vì server chưa có administrator. Luồng này không có mã mời và cũng không có setup token. Người đầu tiên tải trang đó sẽ trở thành administrator của server. Vì vậy, phải giữ cổng đóng cho đến khi bạn đăng ký xong.

Đó là lý do file compose ở trên bind vào 127.0.0.1. Truy cập cổng này qua SSH tunnel từ máy của bạn:

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

Giữ tunnel đang chạy và mở http://127.0.0.1:3010/admin trong trình duyệt trên máy cục bộ. Đăng ký và đăng nhập, sau đó đóng tunnel. Chỉ sau bước này, bạn mới có thể đưa instance lên một hostname public an toàn. Rủi ro chạy đua tương tự cũng xuất hiện trong các app self-host khác. Tình hình còn nghiêm trọng hơn với những app mà lần đăng nhập đầu tiên tạo một passkey gắn với hostname. Vì vậy, bạn phải hoàn tất TLS và domain cuối cùng trước khi tạo tài khoản đầu tiên khi bạn self-host openGym.

AFFiNE lưu dữ liệu ở đâu

Ba đường dẫn chứa toàn bộ dữ liệu và đều nằm trong thư mục bạn đã tạo.

  • ./data/postgres là thư mục dữ liệu Postgres: tài liệu, người dùng, workspace và quyền.
  • ./data/storage được mount vào /root/.affine/storage trong container và chứa mọi file đã upload.
  • ./config được mount vào /root/.affine/config và chứa config.json.

Upstream dùng bind mount thay vì named volume ở đây. Đây là lựa chọn có chủ ý: bạn có thể dùng các lệnh thông thường để tar và sao chép những đường dẫn này, không cần hỏi Docker đã đặt chúng ở đâu. Đổi lại, bạn phải tự xử lý ownership của file trên host. Đây là đánh đổi được đề cập trong bind mount và named volume.

Cách sao lưu AFFiNE

Cần sao lưu 2 loại dữ liệu, và mỗi loại dùng một cách khác nhau. Database là một server đang chạy, nên sao chép các file khi nó đang hoạt động sẽ tạo ra bản sao bị hỏng. Thay vào đó, hãy dump database:

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/

Lệnh dump chạy bên trong container qua local socket, nên không yêu cầu nhập password. Kiểm tra kích thước trong output của ls. File chỉ vài trăm byte nghĩa là dump đã fail nhưng shell vẫn tạo file. Đây là lỗi thường chỉ được phát hiện sau 6 tháng. -T cũng quan trọng: nếu thiếu tùy chọn này, Compose có thể cấp phát terminal và làm hỏng binary stream.

Uploaded files chỉ là các file thông thường, nên hãy tar chúng:

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

Hãy tự sao lưu config.json bằng cách thủ công. Tài liệu AFFiNE, được kiểm tra vào tháng 8 năm 2026, vẫn ghi rằng chức năng export cấu hình từ admin panel chưa được triển khai. Vì vậy, file trên disk là bản sao duy nhất của các thiết lập. Hãy copy cả 3 file ra khỏi server. Bản backup nằm trên cùng disk với dữ liệu mà nó bảo vệ không phải là backup. Mô hình gồm một database dump và một file tar của thư mục uploads này nên được áp dụng cho mọi container stateful khác mà bạn chạy. Đây cũng là cách bảo vệ conversation history và attachments khi bạn tự host Chatwoot làm support desk.

Khôi phục và một bẫy trong các bước được công bố

Hãy đọc các bước khôi phục chính thức trước khi cần dùng, và đọc thật kỹ. Theo nội dung được công bố vào tháng 8 năm 2026, các bước này sao chép một file có tên affine.backup vào container rồi khôi phục từ ./pg.backup. Đây là hai tên khác nhau. Chúng cũng xóa một thư mục ./postgres, trong khi compose file hiện tại lưu dữ liệu tại ./data/postgres. Hãy dùng đúng các path bạn đã sử dụng, thay vì các path trong đoạn hướng dẫn. Đây là trình tự tương ứng với layout trong hướng dẫn này:

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

Lưu ý dùng mv, không phải rm. Khôi phục đè lên một database mà bạn chưa sao lưu sẽ biến một lệnh sai thành mất toàn bộ dữ liệu. Di chuyển thư mục cũ sang tên khác không tốn gì. Đồng thời hãy khôi phục cả uploads bằng tar xzf backup/storage-2026-08-08.tgz -C data, nếu không mọi document sẽ hiển thị attachment bị hỏng. Sau đó đăng nhập và mở một document có chứa image. Đó là bước kiểm tra. Một bản khôi phục mà bạn chưa mở trong browser chỉ là một file, chưa phải backup.

Đặt AFFiNE phía sau proxy đang chạy

AFFiNE sử dụng WebSocket và đây là thành phần bắt buộc. Tài liệu nêu rõ: WebSocket là nền tảng của hệ thống đồng bộ và cộng tác của AFFiNE. Vì vậy, proxy không nâng cấp được các kết nối này sẽ tạo ra workspace mà việc chỉnh sửa âm thầm ngừng đồng bộ. Trang vẫn tải, đăng nhập vẫn hoạt động, nhưng nội dung chỉnh sửa trên một browser không bao giờ xuất hiện trên browser khác. Trong developer tools của browser, mở tab Network và lọc theo WS. Một kết nối liên tục mở rồi đóng là dấu hiệu proxy không chuyển tiếp yêu cầu upgrade.

Nếu bạn đã chạy Traefik cho các container khác, AFFiNE có thể tham gia như một service bình thường. Xóa block ports: khỏi service affine, rồi thêm:

    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'

Ở cuối file, cùng với services:, thêm:

networks:
  proxy:
    external: true

Tên certificate resolver phải khớp với tên được định nghĩa trong cấu hình Traefik. loadbalancer.server.port là cổng container 3010, không phải cổng host. Traefik proxy các kết nối WebSocket mà không cần cấu hình thêm, nên không cần thêm gì nữa. Nếu phần còn lại của stack đã nằm phía sau Authentik để single sign-on, forward auth middleware trên router này sẽ giới hạn quyền truy cập AFFiNE từ browser. Tuy nhiên, hãy tắt middleware đó cho đến khi bạn kiểm tra xong desktop app. Desktop app không có browser session và sẽ không thể đồng bộ. Cách chạy nhiều app phía sau một instance Traefik được trình bày trong một Traefik đứng trước nhiều app.

Với nginx, bạn phải yêu cầu upgrade một cách rõ ràng:

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 mặc định là 1 MB trong nginx. Nếu thiếu dòng này, mọi upload lớn hơn một ảnh nhỏ sẽ fail với status 413 và không có gì xuất hiện trong log AFFiNE, vì request chưa bao giờ đến được AFFiNE. Caddy chỉ cần một dòng, reverse_proxy http://127.0.0.1:3010, và tự xử lý certificate cùng việc upgrade WebSocket.

Những gì bản self-hosted không có

Hãy đánh giá điều này một cách thực tế trước khi chuyển cả team sang dùng.

Tính năng cộng tác theo thời gian thực có sẵn. Đây cũng là tính năng mà toàn bộ khuyến nghị về sizing tập trung vào, vì tài liệu của AFFiNE cho biết mức sử dụng memory đến từ hệ thống đồng bộ và việc merge document. Chỉnh sửa offline là lý do nhiều người muốn dùng công cụ local-first. Ứng dụng desktop có thể thêm server self-hosted của bạn vào danh sách workspace và đăng nhập vào server đó. Hãy kiểm tra đúng hành vi offline mà team của bạn phụ thuộc trước khi triển khai: tắt network, chỉnh sửa trong ứng dụng desktop, kết nối lại, rồi kiểm tra kết quả trên thiết bị thứ hai. Danh sách tính năng không phải là bằng chứng, và danh sách này cũng vậy.

Tính năng full-text search phía server bị tắt trong file compose được cung cấp, trong đó AFFINE_INDEXER_ENABLED=false được đặt trên server và trên migration job. Bật tính năng này cần thêm một container Manticore Search. Đây là service thứ năm và sẽ dùng thêm memory. Trên máy 2 GB, thay đổi này có thể khiến hệ thống vượt quá giới hạn. Search bên trong client vẫn hoạt động với workspace đang mở.

Có 2 giới hạn bạn nên biết trước khi mời người khác. Một workspace self-hosted được cấp tối đa 10 seat. Nếu vượt quá số này, bạn cần Team license của AFFiNE. Tài liệu mô tả blob storage không giới hạn và kích thước blob không giới hạn cho instance self-hosted là các tính năng dự kiến có, nhưng tại thời điểm kiểm tra vào tháng 8 năm 2026 vẫn chưa được triển khai đầy đủ. Cả 2 giới hạn này không đáng kể đối với hộ gia đình hoặc team nhỏ. Nhưng chúng đều quan trọng nếu bạn dự định chuyển 40 người sang dùng.

Nâng cấp

Đọc release notes trước, đặc biệt khi nâng minor version như 0.26 lên 0.27, vì các thay đổi không tương thích có thể xuất hiện. Sao lưu database và thư mục storage trước khi thực hiện bất kỳ thay đổi nào. Job migration sẽ thay đổi schema trong lần khởi động tiếp theo và không thể hoàn tác. Sau đó thay pinned digest, chạy docker compose pull rồi chạy docker compose up -d, và monitor docker compose logs -f affine_migration cho đến khi tiến trình kết thúc mà không có lỗi. docker image prune sẽ xóa các layer cũ sau đó. Một lưu ý lịch sử cho các hệ thống cài đặt từ rất lâu: từ version 0.23.0, image name đã đổi từ affine-graphql thành affine. Vì vậy, compose file cũ hơn version này cần được sửa các dòng image trước khi pull có thể tìm thấy image.

FAQ

AFFiNE container không bao giờ khởi động là vì sao?

Service affine khai báo condition: service_completed_successfully cho job affine_migration. Vì vậy, nếu migration kết thúc với status khác 0, server sẽ không bao giờ khởi động và hoàn toàn không xuất hiện web interface. Chạy docker compose logs affine_migration để xem bước nào đã dừng. Nguyên nhân phổ biến nhất trong compose file được chỉnh sửa thủ công là dùng image postgres mặc định thay cho pgvector/pgvector:pg16. Schema của AFFiNE khai báo extension pgvector và tạo các bảng có column vector(1024) mà Postgres thuần không thể tạo.

AFFiNE tự host cần bao nhiêu RAM?

Trang yêu cầu của AFFiNE đề nghị tối thiểu 4 CPU core và 2 GB RAM. Mức này tăng lên 4 GB khi tài liệu vượt quá 10,000 từ. Trang này cũng ghi nhận rằng việc merge một tài liệu có 10,000 thay đổi có thể dùng đỉnh 1 GB RAM. Trên server 2 GB, chính mức dùng đỉnh này gây lỗi, không phải tải lúc idle: kernel out-of-memory killer dừng tiến trình AFFiNE, rồi restart: unless-stopped khởi động lại tiến trình. Vì vậy, người dùng chỉ thấy trang reload thay vì thông báo lỗi. Xác nhận bằng docker inspect affine_server --format '{{.State.OOMKilled}}'sudo dmesg -T | grep -i 'out of memory', sau đó thêm swap file 2 GB để khi tải tăng đột biến, hệ thống chậm lại thay vì bị dừng.

AFFiNE lưu dữ liệu ở đâu và cần backup những gì?

Ba path bên dưới compose directory chứa toàn bộ dữ liệu: ./data/postgres cho database, ./data/storage cho các file đã upload và ./config cho config.json. Backup database bằng docker compose exec -T postgres pg_dump --format c --username affine affine > affine.dump thay vì copy các file, vì không thể copy an toàn một Postgres đang chạy. Dùng tar cho ./data/storage để backup các file upload. Giữ một bản copy của config.json thủ công, vì tính năng export configuration từ admin panel được ghi là chưa implement tính đến tháng 8 năm 2026.

Collaboration theo thời gian thực có hoạt động trên AFFiNE tự host không?

Có, không cần enable thêm gì. Yêu cầu duy nhất là reverse proxy, vì sync chạy qua các kết nối WebSocket. Với nginx, cần proxy_http_version 1.1 cùng các header UpgradeConnection: upgrade. Traefik và Caddy chuyển tiếp các kết nối này mà không cần configuration bổ sung. Nếu proxy không upgrade các kết nối này, workspace vẫn load và đăng nhập bình thường, nhưng các chỉnh sửa trong một browser không bao giờ xuất hiện trên browser khác.

Có thể chạy AFFiNE với image Postgres mặc định không?

Không. schema.prisma của AFFiNE khai báo extensions = [pgvector(map: "vector")] và định nghĩa 4 table có column embedding với type vector(1024). Migration job vẫn tạo các table này ngay cả khi các tính năng AI bị tắt. Dùng pgvector/pgvector:pg16, đây là Postgres 16 đã được compile kèm extension đó. Nếu trỏ AFFiNE đến một server Postgres bên ngoài, hãy cài pgvector trên server đó và tạo extension trong database đích trước khi chạy migration.