SSD Nodes Learn 🎉 VPS từ $4.99/tháng
Hướng dẫn Matt ConnorBởi Matt Connor

Hướng dẫn tự host AFFiNE bằng Docker Compose chi tiết

Triển khai AFFiNE trên VPS với 4 container gồm Postgres và Redis. Hướng dẫn cấu hình file compose, quản lý dữ liệu, backup và đánh giá hiệu năng thực tế trên RAM 2 GB.

Những gì bạn nhận được khi tự host AFFiNE

Việc tự host AFFiNE mang lại cho bạn một không gian làm việc kiểu Notion trên máy chủ do bạn kiểm soát, chạy dưới dạng bốn container: ứng dụng, một job migration chạy một lần, Postgres và Redis. Tính năng cộng tác thời gian thực được bao gồm, hỗ trợ tối đa 10 người dùng cho một không gian làm việc tự host theo mặc định. Việc cài đặt chỉ cần một file compose và một file cấu hình JSON. Những yếu tố cần cân nhắc là các image tag, bố cục ổ đĩa, giới hạn bộ nhớ và proxy mà bạn đặt phía trước.

AFFiNE giữ trình soạn thảo tài liệu và bảng trắng vô cực trong cùng một không gian làm việc, vì vậy một trang có thể được đọc như tài liệu hoặc trải rộng như bảng trắng. Nếu bạn vẫn đang quyết định nên chạy ứng dụng nào, hãy đọc so sánh các giải pháp thay thế Notion tự host trước. Hướng dẫn này giả định rằng bạn đã đưa ra lựa chọn và tập trung vào việc chạy AFFiNE đúng cách thay vì so sánh lại.

Mọi nội dung tại đây đã được kiểm tra đối chiếu với tài liệu tự host của AFFiNE và các file release được công bố vào ngày 8 tháng 8 năm 2026. Bản release ổn định mới nhất vào ngày đó là 0.27.3, được công bố vào ngày 23 tháng 7 năm 2026.

Chức năng thực tế của bốn container

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

affine_migration là một job chạy một lần, thực thi node ./scripts/self-host-predeploy.js, áp dụng các migration cơ sở dữ liệu rồi thoát. Ứng dụng khai báo condition: service_completed_successfully cho job đó, vì vậy nếu migration thoát với trạng thái khác 0 thì affine sẽ không bao giờ khởi động. Khi giao diện web không hiển thị, log của job đó là thứ đầu tiên cần đọc.

postgres lưu trữ tài liệu, người dùng, không gian làm việc và quyền hạn của bạn. Image được cung cấp là pgvector/pgvector:pg16, đây là Postgres 16 thông thường được biên dịch kèm extension pgvector. pgvector thêm kiểu cột vector vào Postgres, định dạng số được dùng để lưu trữ các embedding nhằm mục đích tìm kiếm văn bản theo ngữ nghĩa.

redis là một dependency bắt buộc: cả server và migration job đều chờ kiểm tra trạng thái (health check) của nó trước khi bắt đầu. Hãy chú ý rằng file compose được cung cấp không cấp volume cho Redis. Mọi dữ liệu bên trong nó sẽ mất sạch sau một lần docker compose down, điều này cho thấy rõ ràng rằng nó không chứa nội dung của bạn và không cần sao lưu.

Tại sao image Postgres là pgvector chứ không phải bản stock postgres

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

AFFiNE chuyển sang sử dụng image pgvector từ phiên bản 0.21. Trên các bản cài đặt cũ hơn, việc chỉ sửa dòng image là chưa đủ để nâng cấp, vì vậy hãy đọc trang hướng dẫn nâng cấp trong tài liệu self-host của AFFiNE trước khi thực hiện pull bất kỳ thứ gì.

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

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

Trang yêu cầu hệ thống của AFFiNE đề nghị tối thiểu 4 nhân CPU và 2 GB RAM, đồng thời tăng bộ nhớ lên 4 GB khi tài liệu của bạn vượt quá 10.000 từ. Trang này cũng giải thích lý do tiêu tốn bộ nhớ: hệ thống đồng bộ và quá trình hợp nhất tài liệu. Có một con số cần ghi nhớ: việc hợp nhất một tài liệu với 10.000 thay đổi có thể tiêu tốn tới 1 GB RAM.

Hãy đối chiếu con số đó với một VPS 2 GB RAM có hai người cùng viết. Mức trung bình thì ổn. Postgres và tiến trình Node vẫn nằm trong giới hạn và còn dư bộ nhớ. Vấn đề nằm ở mức đỉnh (peak). Một lần hợp nhất lớn có thể yêu cầu thêm 1 GB RAM ngoài lượng bộ nhớ đang sử dụng. Trên một máy chủ 2 GB không có swap, OOM (Out-of-Memory) killer của kernel sẽ giải quyết yêu cầu đó bằng cách kill tiến trình lớn nhất, chính là server AFFiNE.

Đồng nghiệp của bạn sẽ không thấy thông báo lỗi. Họ chỉ thấy trang web tải lại, vì restart: unless-stopped sẽ khởi động lại container trong vài giây. Đừng đoán mò, hãy xác nhận bằng cách:

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

true từ lệnh đầu tiên, hoặc một dòng Killed process ghi tên node từ lệnh thứ hai, nghĩa là bạn đã bị thiếu bộ nhớ chứ không phải do lỗi phần mềm. Hãy khắc phục từ cả hai phía. Trước tiên, hãy thêm swap để khi có spike (đột biến tải), hệ thống sẽ chạy chậm lại thay vì bị crash:

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ờ sẽ báo tổng cộng 2.0Gi swap. Swap không làm AFFiNE chạy nhanh hơn và nó cũng không được thiết kế để làm việc đó. Nó biến một cú spike kéo dài một giây thành một giây chạy chậm thay vì làm chết container. Phía còn lại của giải pháp là ngăn Postgres mở rộng cache vào vùng bộ nhớ mà ứng dụng cần khi hợp nhất, đây chính là lúc cần dùng đến 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ố mà 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
  }
]

Việc cài đặt server chiếm 1.5 GB. Một nghìn tài liệu, mỗi tài liệu khoảng một nghìn từ, sẽ thêm 0.1 GB dữ liệu Postgres, con số này gần như không đáng kể. Một nghìn file tải lên sẽ thêm 10 GB, đây mới là phần chính. Đây là các con số kế hoạch được công bố chứ không phải phép đo từ một instance đang chạy, vì vậy hãy coi đó là định hướng thay vì cam kết chính xác. Điều quan trọng là: cơ sở dữ liệu của bạn sẽ luôn nhỏ, còn dung lượng đĩa sẽ phụ thuộc vào các file bạn tải lên.

Tự viết file compose, với các tag được ghim

Tài liệu cài đặt hướng dẫn tải xuống một 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 tốt. Có một chi tiết cần lưu ý trước khi bạn sử dụng nó: tính đến ngày 8 tháng 8 năm 2026, file đính kèm trong bản release 0.27.3 vẫn đọc các đường dẫn từ file .env, sử dụng ${UPLOAD_LOCATION}, ${CONFIG_LOCATION}${DB_DATA_LOCATION}, trong khi trang tham chiếu của tài liệu lại hiển thị một cấu trúc mới hơn, giữ mọi thứ trong ./data và không cần .env chút nào. Cả hai đều là cấu trúc hợp lệ. Việc tự viết file sẽ giải quyết vấn đề này, và dù sao bạn cũng phải chỉnh sửa nó để ghim các image và đặt mật khẩu cơ sở dữ liệu.

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 từ thư mục dự án và thay thế ${DB_PASSWORD} cho bạn, vì vậy mật khẩu không bao giờ xuất hiện trong file mà bạn có thể dán vào một luồng hỗ trợ. Thói quen này nên được duy trì trên mọi stack bạn vận hành, và lý do nằm ở giữ các bí mật bên 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

Có bốn điểm khác biệt so với file mà upstream cung cấp, và mỗi điểm đều có lý do.

  • 127.0.0.1:3010:3010 chỉ publish cổng trên địa chỉ loopback, vì vậy không gì bên ngoài máy chủ có thể truy cập AFFiNE cho đến khi bạn quyết định cách thức. File '3010:3010' của upstream bind vào mọi interface, và trên hầu hết các image VPS, điều đó bao gồm cả interface công cộng.
  • POSTGRES_HOST_AUTH_METHOD: trust đã bị loại bỏ và thay vào đó là một mật khẩu. Xác thực trust chấp nhận mọi kết nối đến cơ sở dữ liệu đó với tư cách là người dùng affine mà không cần mật khẩu. Nó chỉ giới hạn trong mạng Compose nội bộ, điều này ổn cho đến ngày bạn gắn thêm một container khác vào mạng đó hoặc publish cổng 5432 trong khi debug.
  • redis:8-alpine thay thế cho redis đơn thuần, vốn phân giải thành latest. Tính đến tháng 8 năm 2026, đó là Redis 8, vì vậy việc ghim phiên bản giúp giữ lại phiên bản major mà bạn đã kiểm thử và ngăn chặn việc Redis 9 tự động cập nhật trong một lần docker compose pull không liên quan.
  • pgvector/pgvector:pg16 được giữ nguyên như cách upstream thiết lập, vì lý do đã nêu ở trên.

POSTGRES_PASSWORD chỉ được đọc khi Postgres tạo thư mục dữ liệu của nó lần đầu tiên. Trên một instance đã tồn tại, hãy đặt mật khẩu bằng docker compose exec postgres psql -U affine -c "ALTER USER affine WITH PASSWORD 'yourpassword'" và sau đó cập nhật DATABASE_URL cho khớp.

Configuration lives in config/config.json

AFFiNE reads its settings from config/config.json, which is the directory you mounted at /root/.affine/config. Nothing creates that file for you, so write it before the first start. Open ~/affine/config/config.json in an editor and give it this content, with your own domain in place of the example:

{
  "$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 must be the address your users actually open in a browser. AFFiNE builds share links and workspace invitations from that value, so if it is left at http://localhost:3010, an invitation you send points the recipient at their own machine and fails there. Set it to the public HTTPS address before the first start, so the file and the admin panel never disagree about it.

copilot controls the AI features. copilot.byok.enabled is the bring-your-own-key switch, which lets a workspace owner paste their own model provider key into workspace settings. Self-hosting AFFiNE does not include an AI subscription. Leave both false if you do not want it.

Start the stack:

docker compose up -d
docker compose ps

docker compose ps should list affine_postgres and affine_redis as healthy, affine_server as running, and affine_migration_job with the state exited (0). Any other exit code on the migration job is the thing to chase, and its log names the step that stopped:

docker compose logs affine_migration

Ghim image trước khi bạn quên

stable là một tag thay đổi liên tục. Quy trình release của AFFiNE trỏ nhiều tag vào mỗi bản build ổn định, và có hai tag quan trọng ở đây: stable, tag này được trỏ lại sau mỗi lần release, và stable- theo sau là mã hash rút gọn của git, tag này thì không. Nếu để ở stable, một lệnh docker compose pull sau sáu tháng nữa sẽ tải về một image khác và chạy các migration lên database của bạn vào thời điểm mà bạn không hề chọn. Hãy ghim chính xác image mà bạn đã kiểm thử:

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

Lệnh đó sẽ in ra một dòng như ghcr.io/toeverything/affine@sha256: theo sau là một mã hash dài. Hãy dán toàn bộ chuỗi đó vào dòng image: của cả hai file affineaffine_migration. Hai file này phải luôn khớp nhau vì chúng là cùng một image đóng hai vai trò, và nếu không khớp nghĩa là bạn đang migrate database sang một schema này trong khi lại phục vụ nó bằng một schema khác. Việc nâng cấp sau đó sẽ là một thao tác chỉnh sửa có chủ đích thay vì là một sự cố bất ngờ: hãy thay đổi digest, sao lưu, docker compose pull, docker compose up -d.

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

Mở /admin trên một instance mới và AFFiNE sẽ chuyển hướng bạn đến trang tạo tài khoản, vì máy chủ chưa có quản trị viên. Không có mã mời hay setup token nào trong quy trình này. Người đầu tiên tải trang đó sẽ trở thành quản trị viên của máy chủ, vì vậy cổng này phải đóng cho đến khi bạn đã đăng ký xong.

Đó là lý do tại sao file compose ở trên bind vào 127.0.0.1. Hãy truy cập nó thông qua một SSH tunnel từ máy của bạn:

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

Để tiến trình đó chạy và mở http://127.0.0.1:3010/admin trong trình duyệt cục bộ. Đăng ký và đăng nhập, sau đó đóng tunnel lại. Chỉ đến lúc này mới an toàn để đưa instance ra tên miền công khai.

Nơi AFFiNE lưu trữ dữ liệu của bạn

Có ba đường dẫn chứa toàn bộ dữ liệu và tất cả đề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, không gian làm việc, quyền truy cập.
  • ./data/storage được mount tại /root/.affine/storage trong container và chứa mọi tệp tin đã tải lên.
  • ./config được mount tại /root/.affine/config và chứa config.json.

Upstream sử dụng bind mount thay vì named volume tại đây, và lựa chọn này là có chủ đích: bạn có thể nén tar và sao chép các đường dẫn này bằng các lệnh thông thường mà không cần truy vấn Docker xem nó đã đặt chúng ở đâu. Cái giá phải trả là quyền sở hữu tệp tin trên host giờ đây là vấn đề của bạn, đây là sự đánh đổi được đề cập trong bind mount và named volume.

Cách sao lưu AFFiNE

Bạn cần sao lưu hai thành phần và cách thực hiện cho mỗi loại là khác nhau. Cơ sở dữ liệu là một server đang chạy, nên việc copy file trực tiếp khi nó đang hoạt động sẽ tạo ra bản sao bị lỗi. Thay vào đó, hãy thực hiện dump dữ liệu:

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 thông qua socket nội bộ, nên nó sẽ không yêu cầu mật khẩu. Hãy kiểm tra dung lượng file trong kết quả của ls. Một file chỉ có vài trăm byte nghĩa là quá trình dump đã thất bại dù shell vẫn tạo ra file đó; đây là lỗi mà người dùng thường chỉ phát hiện ra sau sáu tháng. Tham số -T cũng rất quan trọng: nếu thiếu nó, Compose có thể cấp phát một terminal và làm hỏng luồng dữ liệu nhị phân.

Các file đã tải lên chỉ là file thông thường, nên hãy nén chúng bằng tar:

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

Hãy tự tay lưu giữ config.json trong bản sao lưu của bạn. Tài liệu của AFFiNE vẫn ghi chú rằng tính năng xuất cấu hình từ bảng điều khiển admin chưa được triển khai (tính đến tháng 8 năm 2026), vì vậy file trên ổ cứng là bản sao duy nhất cho các thiết lập của bạn. Hãy copy cả ba file ra khỏi server. Bản sao lưu nằm trên cùng ổ cứng với dữ liệu gốc không được coi là bản sao lưu.

Khôi phục dữ liệu và một cái bẫy trong các bước hướng dẫn

Hãy đọc kỹ các bước khôi phục chính thức trước khi bạn thực sự cần dùng đến chúng. Theo tài liệu được công bố vào tháng 8 năm 2026, họ sao chép một file tên là affine.backup vào container rồi khôi phục từ ./pg.backup, đây là hai tên khác nhau. Họ cũng xóa một thư mục ./postgres trong khi file compose hiện tại lưu dữ liệu trong ./data/postgres. Hãy tuân thủ các đường dẫn thực tế bạn đã sử dụng thay vì các đường dẫn trong đoạn mã mẫu. Dưới đây là trình tự thực hiện dựa trên cấu trúc của 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 ý sử dụng mv thay vì rm. Việc khôi phục đè lên một database mà bạn chưa sao lưu là cách một lệnh sai lầm dẫn đến mất dữ liệu hoàn toàn. Việc di chuyển thư mục cũ sang một vị trí khác không tốn kém gì cả. Hãy khôi phục cả phần uploads bằng tar xzf backup/storage-2026-08-08.tgz -C data, nếu không mọi tài liệu sẽ hiển thị với các tệp đính kèm bị lỗi. Sau đó, hãy đăng nhập và mở một tài liệu có chứa hình ảnh. Đó chính là bước kiểm tra. Một bản khôi phục mà bạn chưa mở trên trình duyệt chỉ là một file, không phải là bản backup.

Đặt AFFiNE sau một proxy bạn đang chạy

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

Nếu bạn đang chạy Traefik cho các container khác, AFFiNE có thể tham gia như một dịch vụ bình thường. Hãy xóa khối ports: khỏi dịch vụ affine, sau đó 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'

và ở cuối file, cùng với services::

networks:
  proxy:
    external: true

Tên của certificate resolver phải khớp với tên đã định nghĩa trong cấu hình Traefik của bạn, và loadbalancer.server.port là cổng 3010 của container, không phải cổng trên host. Traefik tự động proxy các kết nối WebSocket mà không cần cấu hình thêm, vì vậy không cần làm gì khác. Việc chạy nhiều ứng dụng sau một instance Traefik đã được đề cập tại một Traefik đứng trước nhiều ứng dụng.

Trên nginx, bạn phải yêu cầu nâng cấp kết nối một cách tường minh:

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, vì vậy nếu thiếu dòng đó, mọi file upload lớn hơn một bức ảnh nhỏ sẽ bị lỗi với trạng thái 413 và không có gì xuất hiện trong log của AFFiNE, do request chưa bao giờ đến được ứng dụng. Caddy chỉ cần một dòng, reverse_proxy http://127.0.0.1:3010, và nó tự xử lý chứng chỉ cũng như nâng cấp WebSocket.

Những điểm thiếu sót của bản self-hosted

Hãy thành thật với bản thân về điều này trước khi chuyển cả đội ngũ sang sử dụng.

Tính năng cộng tác thời gian thực đã có sẵn, và đây là tính năng mà mọi lời khuyên về cấu hình đều nhắm đến, vì tài liệu của AFFiNE cho biết mức sử dụng bộ nhớ phụ thuộc vào hệ thống đồng bộ và quá trình gộp tài liệu. Chỉnh sửa ngoại tuyến là lý do nhiều người muốn một công cụ ưu tiên cục bộ (local-first), và ứng dụng desktop có thể thêm server self-hosted của bạn vào danh sách workspace để đăng nhập. Hãy kiểm tra chính xác hành vi ngoại tuyến mà đội ngũ của bạn phụ thuộc vào trước khi quyết định: chỉnh sửa trong ứng dụng desktop khi ngắt mạng, kết nối lại, sau đó 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à điều đó bao gồm cả danh sách này.

Tính năng tìm kiếm toàn văn (full-text search) phía server bị tắt trong file compose được cung cấp, nơi AFFINE_INDEXER_ENABLED=false được thiết lập trên server và trên job migration. Việc bật tính năng này đồng nghĩa với việc thêm một container Manticore Search, đây là service thứ năm và sẽ tốn thêm bộ nhớ. Trên một VPS 2 GB, đây chính là thay đổi khiến hệ thống vượt quá giới hạn tài nguyên. Tìm kiếm bên trong client vẫn hoạt động trên workspace mà bạn đang mở.

Có hai giới hạn cần biết trước khi mời người dùng tham gia. Một workspace self-hosted được cấp tối đa 10 chỗ (seats), và nếu vượt quá con số đó, bạn cần có giấy phép Team từ AFFiNE. Lưu trữ blob không giới hạn và kích thước blob không giới hạn cho các instance self-hosted được mô tả trong tài liệu là dự kiến nhưng chưa được triển khai đầy đủ, tính đến tháng 8 năm 2026. Cả hai điều này không quan trọng đối với hộ gia đình hoặc một đội ngũ nhỏ. Nhưng cả hai đều quan trọng nếu bạn đang có kế hoạch chuyển bốn mươi người sang sử dụng.

Nâng cấp

Hãy đọc ghi chú phát hành trước, đặc biệt là với các bản cập nhật phiên bản nhỏ như từ 0.26 lên 0.27, nơi thường xuất hiện các thay đổi gây lỗi (breaking changes). Hãy sao lưu database và thư mục lưu trữ trước khi thực hiện bất kỳ thao tác nào, vì tiến trình migration sẽ thay đổi schema của bạn ngay lần khởi động tiếp theo và không thể hoàn tác. Sau đó, thay đổi digest đã ghim, chạy docker compose pull theo sau là docker compose up -d, và theo dõi docker compose logs -f affine_migration cho đến khi nó thoát hoàn toàn. docker image prune sẽ xóa các layer cũ sau đó. Một lưu ý lịch sử cho những ai đang dùng bản cài đặt rất cũ: từ phiên bản 0.23.0, tên image đã đổi từ affine-graphql thành affine, vì vậy file compose cũ hơn phiên bản đó cần được viết lại các dòng image trước khi lệnh pull có thể tìm thấy bất kỳ thứ gì.

FAQ

Tại sao container AFFiNE không bao giờ khởi động?

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

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

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

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

Ba đường dẫn trong thư mục compose của bạn chứa mọi thứ: ./data/postgres cho database, ./data/storage cho các file đã tải lên, và ./config cho config.json. Hãy backup database bằng docker compose exec -T postgres pg_dump --format c --username affine affine > affine.dump thay vì copy file trực tiếp, vì không thể copy an toàn một Postgres đang chạy. Nén ./data/storage cho các file tải lên và lưu giữ bản sao của config.json thủ công, vì tính năng xuất cấu hình từ bảng điều khiển admin được liệt kê là chưa hoàn thiện tính đến tháng 8 năm 2026.

Tính năng cộng tác thời gian thực có hoạt động trên AFFiNE tự host không?

Có, và không cần bật thêm bất kỳ tùy chọn nào. Yêu cầu duy nhất là reverse proxy của bạn, vì quá trình đồng bộ chạy qua kết nối WebSocket. Trên nginx, điều này có nghĩa là proxy_http_version 1.1 cùng với các header UpgradeConnection: upgrade, trong khi Traefik và Caddy chuyển tiếp các kết nối này mà không cần cấu hình thêm. Triệu chứng của một proxy không upgrade kết nối là workspace tải và đăng nhập bình thường nhưng các chỉnh sửa thực hiện trên một trình duyệt không bao giờ xuất hiện trên trình duyệt khác.

Tôi 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 bốn bảng với cột embedding kiểu vector(1024), và job migration sẽ tạo các bảng đó ngay cả khi tính năng AI đã tắt. Hãy sử dụng pgvector/pgvector:pg16, đây là Postgres 16 đã được biên dịch sẵn extension đó. Nếu bạn trỏ AFFiNE vào một server Postgres bên ngoài, hãy cài đặt pgvector trên đó và tạo extension trong database đích trước khi chạy migration.