Tự host AFFiNE với Docker Compose trên một VPS
Chạy AFFiNE trên một VPS với Docker Compose: 4 container, image tag cố định, vị trí dữ liệu, backup và giới hạn thực tế của 2 GB RAM.
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 quản lý, chạy bằng 4 container: ứng dụng, 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, với tối đa 10 seat theo mặc định cho một workspace self-hosted. 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 tính trước là image tag, bố cục 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 cân nhắc nên chạy hệ thống nào, hãy đọc bài so sánh các lựa chọn Notion self-hosted 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 so sánh lại.
Toàn bộ nội dung dưới đây đã được đối chiếu với tài liệu self-host của AFFiNE và các file release được công bố vào ngày 8 August 2026. Bản stable mới nhất vào ngày đó là 0.27.3, được phát hành ngày 23 July 2026. Các thông tin bên dưới có ngày cập nhật muộn hơn đã được kiểm tra lại với nguồn chính vào ngày 12 October 2026, khi bản stable mới nhất là 0.27.4, được phát hành ngày 18 August 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 mặc định
Yêu cầu này xuất phát từ schema của AFFiNE, không phải do lựa chọn tùy ý. Trong v0.27.4, schema.prisma vẫn khai báo extensions = [pgvector(map: "vector")], nhưng không chứa cột embedding nào. Bốn model có cột embedding với kiểu vector(1024) trong v0.27.3 đã không còn trong v0.27.4. Hãy giữ image pgvector/pgvector:pg16 được cung cấp: extension này vẫn được khai báo, còn postgres:16 mặc định không có extension đó.
Template v0.27.4 chính thức dùng image pgvector; template v0.21.0 vẫn dùng postgres:16. Nếu đang chạy bản cài đặt cũ, sửa dòng image không đủ để nâng cấp. 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 pull bất cứ thứ gì.
Còn một điểm cần lưu ý về tag đó. pg16 nghĩa là Postgres 16, và major version của Postgres không phải là con số có thể tăng tùy ý. 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 tương tự The data directory was initialized by PostgreSQL version 16, which is not compatible with this version 17 trong docker compose logs postgres. Việc chuyển major version cần dùng một trong ba cách được nêu trong release notes của PostgreSQL 17: dump rồi restore vào data directory mới, pg_upgrade hoặc logical replication. Với một cài đặt Compose đơn lẻ, dump và restore thường là cách đơn giản nhất, vì pg_upgrade cần binary của server cũ và mới chạy song song.
AFFiNE cần bao nhiêu CPU và RAM khi tự host
Trang yêu cầu của AFFiNE đề xuất tối thiểu 4 core CPU và 2 GB RAM. Khi tài liệu vượt quá 10,000 từ, mức RAM cần thiết tăng lên 4 GB. Trang này cũng nêu rõ RAM được dùng cho việc gì: hệ thống đồng bộ và quá trình merge tài liệu. Có một con số đáng nhớ: việc merge tài liệu có 10,000 thay đổi có thể làm mức sử dụng RAM tăng vọt lên 1 GB.
Hãy đối chiếu con số đó với một gói 2 GB có 2 người cùng viết. Mức sử dụng trung bình thường 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ư RAM. Vấn đề là mức đỉnh. Một lần merge lớn có thể đẩy mức sử dụng RAM của server lên khoảng 1 GB. Trên máy 2 GB không có swap, mức đỉnh đó cộng với Postgres và các tiến trình khác đang resident có thể làm cạn RAM. Khi đó kernel's out-of-memory (OOM) killer sẽ kill một tiến trình để giải phóng RAM. AFFiNE server là tiến trình lớn nhất nên có khả năng bị kill cao nhất. Tuy nhiên, tài liệu Docker nêu rõ bất kỳ tiến trình nào cũng có thể bị chọn, kể cả Docker.
Khi AFFiNE server bị kill, đồng nghiệp của bạn không thấy lỗi. Họ chỉ thấy trang tự 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 kết quả của lệnh đầu tiên, hoặc một dòng Killed process có tên node trong kết quả của lệnh thứ hai, nghĩa là server đã hết RAM chứ không phải gặp bug. Hãy xử lý từ cả hai phía. Trước tiên, thêm swap để khi có spike, hệ thống có vùng đệm để ghi tạm trước khi OOM killer hoạt động:
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 -hfree -h hiện phải báo tổng swap là 2.0Gi. Swap không làm AFFiNE chạy nhanh hơn, và nó không được thiết kế cho mục đích đó. Swap cung cấp chỗ tạm thời cho các spike, nhờ vậy spike có nhiều khả năng chỉ làm chậm một giây thay vì làm container chết. Đây là vùng đệm, không phải sự bảo đảm. Phần còn lại của cách xử lý là ngăn Postgres mở rộng cache vào vùng RAM mà ứng dụng cần khi merge. Đó là mục đích của giới hạn bộ nhớ trên một service Compose. Nếu vẫn tiếp tục bị kill sau khi thực hiện cả hai bước, server cần 4 GB như mức yêu cầu của AFFiNE khi xử lý tài liệu lớn hơn.
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 đó:
Dữ liệu đằng sau biểu đồ này
[
{
"label": "Server install",
"gb": 1.5
},
{
"label": "Postgres per 1,000 docs",
"gb": 0.1
},
{
"label": "Blob store per 1,000 uploads",
"gb": 10
}
]Phần cài đặt server chiếm 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à đây mới là phần chính. Đây là các số liệu dùng để lập kế hoạch do AFFiNE công bố, không phải kết quả đo từ một instance đang chạy. Vì vậy, hãy xem chúng là ước lượng về xu hướng chứ không phải cam kết chính xác. Điều quan trọng là xu hướng đó: database của bạn vẫn nhỏ, còn các 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 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. Nhưng trước khi dựa vào file đó, bạn nên biết một chi tiết: tính đến ngày 8 tháng 8 năm 2026, file đính kèm trong release 0.27.3 vẫn đọc các path từ file .env, bằng cách dùng ${UPLOAD_LOCATION}, ${CONFIG_LOCATION} và ${DB_DATA_LOCATION}, trong khi trang tham chiếu của tài liệu đã hiển thị layout mới hơn, đặt dữ liệu storage và PostgreSQL dưới ./data, giữ configuration dưới ./config và không cần .env. Release 0.27.4 đã chuyển file đính kèm sang layout đó. Tính đến ngày 12 tháng 10 năm 2026, hai bên đã khớp nhau. Nếu bạn cài từ file cũ, các path của bạn vẫn lấy từ .env. Trang nâng cấp của AFFiNE cảnh báo rằng nếu thay file compose mà không mang các thiết lập đó sang, Postgres có thể khởi động với data directory rỗng. Tự viết file giúp loại bỏ hoàn toàn vấn đề này. Bạn cũng phải 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 .envCompose tự đọc .env từ project directory và tự 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. Đây là thói quen nên duy trì cho mọi stack bạn chạy. Phần giải thích nằm trong giữ secret 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-stoppedCó 4 điểm khác với file upstream cung cấp. Mỗi điểm đều có lý do.
127.0.0.1:3010:3010chỉ publish port trên địa chỉ loopback. Các bản Docker trước 28.0.0 cho phép host trên cùng L2 segment truy cập các port được publish trên localhost. Direct routing được cấu hình cũng có thể expose container port dù port chỉ được publish trên loopback. Hãy dùng Docker 28.0.0 trở lên và tắt direct routing trong quá trình cài đặt.'3010:3010'của upstream bind trên mọi interface. Trên phần lớn image VPS, trong đó có cả interface public.POSTGRES_HOST_AUTH_METHOD: trustđã bị loại bỏ và thay bằng password. Cơ chế trust authentication chấp nhận mọi connection đến database đó dưới useraffinemà 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 5432 để debug.redis:8-alpinethay chorediskhông có version cố định, vốn resolve thànhlatest. Tính đến tháng 8 năm 2026, đó là Redis 8. Việc pin version giữ nguyên major version bạn đã kiểm thử và ngăn Redis 9 được cài trong một lầndocker compose pullkhô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 khởi động đầu tiên. Mở ~/affine/config/config.json bằng trình soạn thảo và ghi nội dung sau, thay domain mẫu 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 tạo share link và lời mời vào workspace từ giá trị này. Nếu để nguyên http://localhost:3010, lời mời bạn gửi sẽ trỏ người nhận về chính máy của họ và không hoạt động. Hãy đặt địa chỉ HTTPS public trước lần khởi động đầu tiên, để file cấu hình và admin panel luôn thống nhất về giá trị này. Những thiết lập bạn thay đổi sau đó trong admin panel được lưu vào database, không được ghi ngược vào file này.
copilot điều khiển các tính năng AI. copilot.byok.enabled là tùy chọn cho phép người dùng tự cung cấp key, để chủ sở hữu workspace nhập key của model provider vào phần cài đặt workspace. Việc self-host AFFiNE không bao gồm gói đăng ký AI. Nếu không cần tính năng này, hãy để cả hai false.
Khởi động stack:
docker compose up -d
docker compose ps --alldocker compose ps --all phải liệt kê affine_postgres và affine_redis ở trạng thái healthy, affine_server ở trạng thái running, và affine_migration_job với trạng thái exited (0). Tùy chọn --all rất quan trọng: nếu không có tùy chọn này, docker compose ps chỉ hiển thị các container đang chạy. Vì vậy, migration job đã hoàn tất thành công sẽ không xuất hiện. Nếu migration job trả về exit code khác, đó là lỗi cần điều tra. Log của job sẽ cho biết bước đã dừng:
docker compose logs affine_migrationGhim 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ả affine và affine_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 quản trị trước khi người khác làm việc đó
Mở /admin trên một instance mới, AFFiNE sẽ chuyển bạn đến trang tạo tài khoản vì server chưa có administrator. Quy trình này không có invitation code hoặc setup token. Người đầu tiên hoàn tất đăng ký 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 vào đó qua SSH tunnel từ máy của bạn:
ssh -L 3010:127.0.0.1:3010 you@your-server-ipGiữ phiên tunnel đang chạy và mở http://127.0.0.1:3010/admin trong trình duyệt cục bộ. Đăng ký rồi đăng nhập, sau đó đóng tunnel. Chỉ khi đó bạn mới có thể an toàn đưa instance lên một hostname public. Các ứng dụng self-host khác cũng có cùng rủi ro tranh chấp này. Rủi ro còn cao hơn với những ứng dụng 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 tự host openGym.
Vị trí AFFiNE lưu dữ liệ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/postgreslà thư mục dữ liệu Postgres: tài liệu, người dùng, workspace, quyền và mọi thiết lập server được lưu từ admin panel../data/storageđược mount tại/root/.affine/storagetrong container và chứa mọi file đã upload../configđược mount tại/root/.affine/configvà chứaconfig.jsoncùngprivate.keydo migration job tạo ra.
Upstream dùng bind mount thay vì named volume ở đây, và đây là lựa chọn có chủ đí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 mà 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à trade-off được giải thích trong bind mount và named volume.
Cách sao lưu AFFiNE
Cần sao lưu hai loại dữ liệu và mỗi loại có cách sao lưu khác nhau. Database là một server đang hoạt động, nên việc copy các file khi database đang chạy 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à lệnh dump đã fail nhưng shell vẫn tạo file. Đây là lỗi mà nhiều người chỉ phát hiện sau sáu tháng. -T cũng rất 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 file chỉ là các file thông thường, nên hãy dùng tar:
tar czf backup/storage-$(date +%F).tgz -C data storage
tar czf backup/config-$(date +%F).tgz config docker-compose.yml .envGiữ toàn bộ thư mục config, Compose file và .env trong bản sao lưu, vì dump không chứa các thành phần này. Migration job cũng tạo config/private.key, nên nếu chỉ copy config.json thì file đó sẽ bị bỏ sót. Các setting được lưu trong admin panel nằm trong database, nên dump ở trên đã bao gồm chúng. Từ ngày 12 October 2026, trang backup của AFFiNE cũng cho phép export các server setting đã chỉnh sửa từ admin panel. Copy cả ba file ra khỏi server. Bản sao lưu nằm trên cùng disk với dữ liệu mà nó bảo vệ thì không phải là bản sao lưu. Cách tách riêng database dump và tar của thư mục uploads này nên được áp dụng cho mọi stateful container khác mà bạn chạy. Đây cũng là cách bảo vệ conversation history và attachment 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ô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ỹ. Trong phiên bản được công bố vào tháng 8 năm 2026, tài liệu 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. Tài liệu cũng xóa thư mục ./postgres, trong khi file compose hiện tại lưu dữ liệu trong ./data/postgres. Tính đến ngày 12 tháng 10 năm 2026, trang tài liệu đã dùng thống nhất affine.backup và yêu cầu giữ lại thư mục Postgres cũ cho đến khi xác minh xong việc khôi phục. Vì vậy, lỗi này đã được sửa ở upstream, nhưng bản sao của các bước được lưu trước thời điểm đó vẫn còn lỗi. Dù thế nào, hãy dùng đúng các đường dẫn bạn đã sử dụng, không dùng các đường dẫn trong bất kỳ snippet nào. Dưới đây là trình tự áp dụng cho cấu trúc 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 -dLưu ý dùng mv thay vì rm. Khôi phục đè lên database mà bạn chưa sao lưu là cách 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 vị trí khác không tốn chi phí. Đồng thời khôi phục cả các file upload 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ị attachment bị hỏng. Sau đó đăng nhập và mở một tài liệu có chứa hình ảnh. Đó mới là bước kiểm tra. Một bản khôi phục chưa được 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: trueTê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 tự host không có
Hãy đánh giá trung thực điểm này 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ộ hướng dẫn 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 sync và quá trình 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 tự host 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 vào 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 bằng chứng, và danh sách này cũng vậy.
Full-text search phía server đang tắt trong file compose được cung cấp, trong đó AFFINE_INDEXER_ENABLED=false được đặt trên server và job migration. 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 trong client vẫn hoạt động trên 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 tự host được cấp tối đa 10 seat. Vượt quá mức này cần có 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 tự host là các tính năng dự kiến nhưng chưa được triển khai đầy đủ, theo thông tin kiểm tra vào tháng 10 năm 2026. Cả 2 giới hạn này không quan trọng với hộ gia đình hoặc team nhỏ. Nhưng cả 2 đều quan trọng nếu bạn dự định chuyển 40 người sang dùng.
Nâng cấp
Đọc ghi chú phát hành trước, đặc biệt khi nâng cấp phiên bản phụ, chẳng hạn từ 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 thay đổi, vì migration job sẽ thay đổi schema ở lần khởi động tiếp theo và không thể hoàn tác. Sau đó thay đổi digest đã pin, chạy docker compose pull rồi docker compose up -d, và theo dõi docker compose logs -f affine_migration cho đến khi lệnh kết thúc bình thường. 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ừ version 0.23.0, tên image đã đổi từ affine-graphql thành affine. Vì vậy, compose file cũ hơn phiên bản đó cần được sửa lại các dòng image trước khi pull có thể tìm thấy image. Từ version 0.27.4, compose template chính thức đã chuyển các path của Postgres, upload và configuration, đồng thời thay đổi kết nối database mặc định. Hãy so sánh file mới với file hiện tại thay vì chép đè lên một bản cài đặt đang hoạt động.
FAQ
Vì sao container AFFiNE không bao giờ khởi động?
Service affine khai báo condition: service_completed_successfully trên job affine_migration. Vì vậy, nếu migration kết thúc với bất kỳ status nào khác 0, server sẽ không bao giờ khởi động và web interface cũng không xuất hiện. Chạy docker compose logs affine_migration để xem bước nào đã dừng. Kiểm tra file compose đã chỉnh sửa thủ công vẫn sử dụng pgvector/pgvector:pg16. Postgres mặc định không có extension pgvector mà schema của AFFiNE vẫn khai báo, dù v0.27.4 không còn định nghĩa 4 model embedding có trong v0.27.3.
AFFiNE self-hosted cần bao nhiêu RAM?
Trang yêu cầu của AFFiNE đề nghị ít nhất 4 CPU core và 2 GB RAM. Mức này tăng lên 4 GB khi tài liệu dài hơn 10,000 từ. Trang này cũng lưu ý rằng việc merge một tài liệu có 10,000 thay đổi có thể dùng tới 1 GB RAM. Trên server 2 GB, chính mức tăng đột biến này gây lỗi, không phải mức tải khi idle. Kernel OOM killer sẽ chọn một process để dừng, nhiều khả năng là AFFiNE server vì đây là process lớn nhất. Sau đó restart: unless-stopped khởi động lại process này, nên người dùng chỉ thấy trang reload thay vì lỗi. Xác nhận bằng docker inspect affine_server --format '{{.State.OOMKilled}}' và sudo dmesg -T | grep -i 'out of memory'. Sau đó thêm file swap 2 GB để đệm các mức tăng tải ngắn hạn. Nếu process vẫn bị kill, hãy nâng server lên 4 GB RAM.
AFFiNE lưu dữ liệu ở đâu và tôi cần backup những gì?
3 path bên dưới thư mục compose chứa toàn bộ dữ liệu: ./data/postgres cho database, ./data/storage cho file đã upload, và ./config cho config.json cùng private.key được tạo tự động. 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. Dump này cũng chứa mọi setting đã lưu trong admin panel, vì AFFiNE lưu các setting đó trong database. Dùng tar cho ./data/storage để backup file upload. Giữ toàn bộ thư mục config, file Compose và .env, vì các file này không nằm trong database.
Collaboration theo thời gian thực có hoạt động trên AFFiNE self-hosted không?
Có, và không cần bật thêm tùy chọn nào. Yêu cầu duy nhất nằm ở reverse proxy, vì sync sử dụng kết nối WebSocket. Với nginx, cần proxy_http_version 1.1 cùng các header Upgrade và Connection: upgrade. 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. Nếu proxy không upgrade được kết nối, 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 xuất hiện ở browser khác.
Tôi có thể chạy AFFiNE với image Postgres mặc định không?
Hãy dùng pgvector/pgvector:pg16. Đây là Postgres 16 đã được build kèm extension pgvector. AFFiNE v0.27.4 vẫn khai báo extensions = [pgvector(map: "vector")] trong schema.prisma, nhưng không còn định nghĩa 4 model có các cột embedding vector(1024) từng có trong v0.27.3. Postgres mặc định không có extension đã khai báo. Nếu trỏ AFFiNE đến một Postgres server 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.