SSD Nodes Learn 🎉 VPS เริ่ม $5.50/เดือน
คู่มือ Matt Connorโดย Matt Connor · อัปเดตเมื่อ 2026-08-13

วิธีติดตั้ง AFFiNE บน VPS ด้วย Docker Compose

เรียนรู้วิธีติดตั้ง AFFiNE บนเซิร์ฟเวอร์ส่วนตัวผ่าน Docker Compose ครบทั้ง 4 คอนเทนเนอร์ การตั้งค่า image tags การจัดการข้อมูล และการประเมินการใช้ RAM 2 GB สำหรับการใช้งานจริง

สิ่งที่คุณจะได้รับเมื่อโฮสต์ AFFiNE ด้วยตนเอง

การโฮสต์ AFFiNE ด้วยตนเองช่วยให้คุณมีพื้นที่ทำงานสไตล์ Notion บนเซิร์ฟเวอร์ที่คุณควบคุมได้ โดยทำงานในรูปแบบคอนเทนเนอร์ 4 รายการ ได้แก่ ตัวแอปพลิเคชัน, งาน migration แบบครั้งเดียว, Postgres และ Redis ระบบรองรับการทำงานร่วมกันแบบเรียลไทม์ โดยรองรับผู้ใช้สูงสุด 10 ที่นั่งสำหรับพื้นที่ทำงานที่โฮสต์เองตามค่าเริ่มต้น การติดตั้งใช้ไฟล์ compose หนึ่งไฟล์และไฟล์ config รูปแบบ JSON อีกหนึ่งไฟล์ สิ่งที่ต้องพิจารณาคือ image tags, การจัดวางดิสก์, ขีดจำกัดของหน่วยความจำ และ proxy ที่คุณนำมาวางไว้ด้านหน้า

AFFiNE รวมโปรแกรมแก้ไขเอกสารและพื้นที่ทำงานแบบไม่จำกัด (infinite canvas) ไว้ในพื้นที่ทำงานเดียวกัน ทำให้หน้าเอกสารหนึ่งหน้าสามารถอ่านในรูปแบบเอกสารหรือกางออกเป็นกระดานไวท์บอร์ดได้ หากคุณยังตัดสินใจไม่ได้ว่าจะเลือกใช้งานซอฟต์แวร์ใด ให้ลองอ่าน การเปรียบเทียบทางเลือกแทน Notion สำหรับการโฮสต์เอง ก่อน คู่มือนี้สมมติว่าคุณได้ตัดสินใจเลือกแล้ว และเน้นไปที่การรัน AFFiNE อย่างถูกต้องแทนที่จะเปรียบเทียบซ้ำอีกครั้ง

เนื้อหาทั้งหมดในที่นี้ได้รับการตรวจสอบกับเอกสารการโฮสต์ AFFiNE ด้วยตนเองและไฟล์ release ที่เผยแพร่เมื่อวันที่ 8 สิงหาคม 2026 โดย release ที่เสถียรล่าสุด ณ วันดังกล่าวคือ 0.27.3 ซึ่งเผยแพร่เมื่อวันที่ 23 กรกฎาคม 2026

หน้าที่ของคอนเทนเนอร์ทั้งสี่ตัว

affine คือเซิร์ฟเวอร์และเว็บไคลเอนต์ที่รวมอยู่ในอิมเมจเดียว โดยจะรอรับการเชื่อมต่อที่พอร์ต 3010

affine_migration เป็นงานแบบ one-shot ที่รัน node ./scripts/self-host-predeploy.js เพื่อปรับปรุงโครงสร้างฐานข้อมูล (database migrations) แล้วจบการทำงาน แอปพลิเคชันได้ประกาศ condition: service_completed_successfully ไว้ในงานดังกล่าว ดังนั้นหากการ migration จบลงด้วยสถานะที่ไม่ใช่ศูนย์ จะส่งผลให้ affine ไม่เริ่มทำงานเลย หากหน้าเว็บอินเทอร์เฟซไม่แสดงขึ้นมา ให้ตรวจสอบ log ของงานนี้เป็นอันดับแรก

postgres เป็นที่เก็บเอกสาร ผู้ใช้ พื้นที่ทำงาน และสิทธิ์การเข้าถึงของคุณ อิมเมจที่มาพร้อมกับซอฟต์แวร์คือ pgvector/pgvector:pg16 ซึ่งเป็น Postgres 16 มาตรฐานที่คอมไพล์รวมกับส่วนขยาย pgvector ไว้แล้ว pgvector จะเพิ่มชนิดข้อมูล vector เข้าไปใน Postgres ซึ่งเป็นรูปแบบตัวเลขที่ใช้เก็บ embeddings เพื่อให้สามารถค้นหาข้อความตามความหมายได้

redis เป็น dependency ที่จำเป็น: ทั้งเซิร์ฟเวอร์และงาน migration จะรอให้ health check ของบริการนี้ผ่านก่อนจึงจะเริ่มทำงาน สังเกตว่าไฟล์ compose ที่มาพร้อมกับซอฟต์แวร์ไม่ได้กำหนด volume ให้กับ Redis ข้อมูลภายในนั้นจะไม่คงอยู่หลังจากการ docker compose down ซึ่งบ่งบอกชัดเจนว่ามันไม่ได้เก็บเนื้อหาของคุณและไม่จำเป็นต้องสำรองข้อมูล

เหตุผลที่อิมเมจ Postgres ต้องเป็น pgvector ไม่ใช่ stock postgres

ความต้องการนี้มาจากโครงสร้างข้อมูล (schema) ของ AFFiNE ไม่ใช่ความชอบส่วนตัว ใน schema.prisma แหล่งข้อมูลได้ประกาศ extensions = [pgvector(map: "vector")] ไว้ และมีตาราง 4 ตารางที่ใช้คอลัมน์ embedding ซึ่งกำหนดชนิดข้อมูลเป็น vector(1024) งาน migration จะสร้างตารางเหล่านั้นขึ้นมาไม่ว่าคุณจะเปิดใช้งานฟีเจอร์ AI หรือไม่ก็ตาม ดังนั้นส่วนขยาย (extension) นี้จึงต้องมีอยู่ในฐานข้อมูลก่อนที่การ migration จะเสร็จสิ้น หากคุณเปลี่ยนไปใช้ postgres:16 ส่วนขยายดังกล่าวจะหายไป ทำให้การ migration ไม่สามารถสร้างคอลัมน์เหล่านั้นได้ และเซิร์ฟเวอร์จะค้างอยู่กับการรอคอยงานที่ล้มเหลวไปแล้ว

AFFiNE เปลี่ยนมาใช้อิมเมจ pgvector ตั้งแต่เวอร์ชัน 0.21 สำหรับการติดตั้งที่เก่ากว่านั้น การแก้ไขบรรทัดอิมเมจเพียงอย่างเดียวไม่ถือว่าเป็นการอัปเกรดที่สมบูรณ์ ดังนั้นโปรดอ่านหน้าการอัปเกรดใน เอกสารการ self-host ของ AFFiNE ก่อนที่คุณจะดึงอิมเมจใดๆ มาใช้งาน

อีกเรื่องหนึ่งเกี่ยวกับแท็กดังกล่าว pg16 หมายถึง Postgres 16 ซึ่งเวอร์ชันหลัก (major version) ของ Postgres ไม่ใช่ตัวเลขที่คุณจะปรับเพิ่มขึ้นได้ตามใจ หากคุณเปลี่ยนเป็น pg17 บนไดเรกทอรีข้อมูลเดิม Postgres จะปฏิเสธการเริ่มทำงาน โดยจะแสดงข้อความในลักษณะ The data directory was initialized by PostgreSQL version 16, which is not compatible with this version 17 ใน docker compose logs postgres การเปลี่ยนเวอร์ชันหลักจำเป็นต้องใช้วิธีการ dump ข้อมูลออกมาแล้ว restore เข้าสู่ไดเรกทอรีข้อมูลใหม่เท่านั้น

AFFiNE ที่โฮสต์เองต้องการ CPU และ RAM เท่าใด

หน้าความต้องการของระบบของ AFFiNE ระบุว่าต้องการ CPU อย่างน้อย 4 คอร์ และ RAM 2 GB และจะเพิ่มความต้องการหน่วยความจำเป็น 4 GB เมื่อเอกสารของคุณมีเนื้อหาเกิน 10,000 คำ หน้าเดียวกันนี้ระบุว่าหน่วยความจำถูกใช้ไปกับอะไรบ้าง นั่นคือระบบซิงค์และการรวมเอกสาร (document merging) มีตัวเลขหนึ่งที่ควรจำไว้คือ การรวมเอกสารที่มีการแก้ไข 10,000 รายการอาจใช้หน่วยความจำสูงสุดถึง 1 GB

เมื่อพิจารณาเทียบกับแผน 2 GB ที่มีผู้เขียนสองคน ค่าเฉลี่ยถือว่าใช้งานได้ปกติ Postgres และกระบวนการ Node จะทำงานอยู่ภายใต้ขีดจำกัดและยังมีพื้นที่เหลือ แต่ปัญหาจะอยู่ที่ช่วงพีค (peak) การรวมเอกสารขนาดใหญ่เพียงรายการเดียวอาจเรียกใช้หน่วยความจำเพิ่มอีก 1 GB นอกเหนือจากที่ถูกใช้งานอยู่เดิม และบนเครื่อง 2 GB ที่ไม่มี swap ตัว OOM (out-of-memory) killer ของ kernel จะตอบสนองต่อคำขอนั้นด้วยการสั่งปิดกระบวนการที่ใช้หน่วยความจำมากที่สุด ซึ่งก็คือเซิร์ฟเวอร์ AFFiNE

เพื่อนร่วมงานของคุณจะไม่เห็นข้อความแจ้งเตือนข้อผิดพลาด แต่จะเห็นหน้าเว็บโหลดใหม่ เนื่องจาก restart: unless-stopped จะนำ container กลับมาทำงานใหม่ภายในไม่กี่วินาที อย่าเดาสาเหตุ ให้ยืนยันด้วยวิธีนี้:

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

true จากคำสั่งแรก หรือบรรทัด Killed process ที่ระบุชื่อ node จากคำสั่งที่สอง หมายความว่าหน่วยความจำของคุณหมด ไม่ใช่เพราะพบข้อผิดพลาดในโปรแกรม ให้แก้ไขจากทั้งสองด้าน ขั้นแรกให้เพิ่ม swap เพื่อให้ช่วงที่หน่วยความจำพุ่งสูงขึ้นกลายเป็นความช้าแทนที่จะเป็นความล้มเหลว:

sudo fallocate -l 2G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
free -h

free -h ควรแสดงผลรวม swap ที่ 2.0Gi แล้วในขณะนี้ Swap ไม่ได้ทำให้ AFFiNE ทำงานเร็วขึ้น และไม่ได้ถูกออกแบบมาเพื่อจุดประสงค์นั้น แต่มันเปลี่ยนช่วงพีคที่ทำให้ระบบล่มในหนึ่งวินาทีให้กลายเป็นความหน่วงเพียงชั่วครู่แทนที่จะทำให้ container ตาย การแก้ไขอีกด้านคือการหยุดไม่ให้ Postgres ขยายแคชเข้าไปในพื้นที่ที่แอปพลิเคชันต้องการในขณะรวมเอกสาร ซึ่งเป็นสิ่งที่ การจำกัดหน่วยความจำบนบริการ Compose มีไว้เพื่อการนี้

การจัดเก็บข้อมูลนั้นคาดการณ์ได้ง่ายกว่ามาก นี่คือตัวเลขที่ AFFiNE เผยแพร่ไว้ในหน้าเดียวกัน:

ChartPublished AFFiNE storage figures, August 2026
The data behind this chart
[
  {
    "label": "Server install",
    "gb": 1.5
  },
  {
    "label": "Postgres per 1,000 docs",
    "gb": 0.1
  },
  {
    "label": "Blob store per 1,000 uploads",
    "gb": 10
  }
]

การติดตั้งเซิร์ฟเวอร์ใช้พื้นที่ 1.5 GB เอกสารหนึ่งพันฉบับที่แต่ละฉบับมีเนื้อหาประมาณหนึ่งพันคำจะเพิ่มข้อมูล Postgres อีก 0.1 GB ซึ่งถือว่าน้อยมาก ไฟล์ที่อัปโหลดหนึ่งพันไฟล์จะเพิ่มพื้นที่อีก 10 GB ซึ่งเป็นปัจจัยหลักทั้งหมด ตัวเลขเหล่านี้เป็นตัวเลขสำหรับการวางแผนที่เผยแพร่ไว้ ไม่ใช่การวัดผลจาก instance ที่กำลังทำงานจริง ดังนั้นให้มองว่าเป็นแนวทางมากกว่าคำมั่นสัญญา สิ่งสำคัญคือรูปแบบของมัน: ฐานข้อมูลของคุณจะมีขนาดเล็ก และไฟล์ที่คุณอัปโหลดจะเป็นตัวกำหนดขนาดดิสก์ของคุณ

เขียนไฟล์ compose ด้วยตนเองพร้อมระบุ tag แบบเจาะจง

วิธีการติดตั้งตามเอกสารแนะนำให้ดาวน์โหลดไฟล์สำเร็จรูปด้วย curl -L -o docker-compose.yml https://github.com/toeverything/AFFiNE/releases/latest/download/docker-compose.yml ซึ่งใช้งานได้จริง แต่มีรายละเอียดหนึ่งที่ควรทราบก่อนตัดสินใจใช้งาน: ณ วันที่ 8 สิงหาคม 2026 ไฟล์ที่แนบมากับ release 0.27.3 ยังคงอ่าน path จากไฟล์ .env โดยใช้ ${UPLOAD_LOCATION}, ${CONFIG_LOCATION} และ ${DB_DATA_LOCATION} ในขณะที่หน้าอ้างอิงในเอกสารแสดงโครงสร้างแบบใหม่ที่เก็บทุกอย่างไว้ภายใต้ ./data และไม่จำเป็นต้องใช้ .env เลย ทั้งสองแบบเป็นโครงสร้างที่ถูกต้อง การเขียนไฟล์ด้วยตนเองจะช่วยตัดปัญหาความสับสนนี้ และคุณยังต้องแก้ไขไฟล์เพื่อระบุเวอร์ชันของ image (pinning) และตั้งรหัสผ่านฐานข้อมูลอยู่ดี

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

Compose จะอ่าน .env จากไดเรกทอรีโปรเจกต์โดยอัตโนมัติและแทนที่ค่า ${DB_PASSWORD} ให้คุณ ดังนั้นรหัสผ่านจะไม่ปรากฏในไฟล์ที่คุณอาจนำไปแปะในกระทู้ขอความช่วยเหลือ นิสัยนี้ควรนำไปใช้กับทุก stack ที่คุณรัน และเหตุผลอธิบายไว้ใน การเก็บความลับไว้นอกไฟล์ compose

ตอนนี้ให้เขียนไฟล์ ~/affine/docker-compose.yml:

name: affine
services:
  affine:
    image: ghcr.io/toeverything/affine:stable
    container_name: affine_server
    ports:
      - '127.0.0.1:3010:3010'
    depends_on:
      redis:
        condition: service_healthy
      postgres:
        condition: service_healthy
      affine_migration:
        condition: service_completed_successfully
    volumes:
      - ./data/storage:/root/.affine/storage
      - ./config:/root/.affine/config
    environment:
      - REDIS_SERVER_HOST=redis
      - DATABASE_URL=postgresql://affine:${DB_PASSWORD}@postgres:5432/affine
      - AFFINE_INDEXER_ENABLED=false
    restart: unless-stopped

  affine_migration:
    image: ghcr.io/toeverything/affine:stable
    container_name: affine_migration_job
    command: ['sh', '-c', 'node ./scripts/self-host-predeploy.js']
    volumes:
      - ./data/storage:/root/.affine/storage
      - ./config:/root/.affine/config
    environment:
      - REDIS_SERVER_HOST=redis
      - DATABASE_URL=postgresql://affine:${DB_PASSWORD}@postgres:5432/affine
      - AFFINE_INDEXER_ENABLED=false
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy

  redis:
    image: redis:8-alpine
    container_name: affine_redis
    healthcheck:
      test: ['CMD', 'redis-cli', '--raw', 'incr', 'ping']
      interval: 10s
      timeout: 5s
      retries: 5
    restart: unless-stopped

  postgres:
    image: pgvector/pgvector:pg16
    container_name: affine_postgres
    volumes:
      - ./data/postgres:/var/lib/postgresql/data
    environment:
      POSTGRES_USER: affine
      POSTGRES_PASSWORD: ${DB_PASSWORD}
      POSTGRES_DB: affine
      POSTGRES_INITDB_ARGS: '--data-checksums'
    healthcheck:
      test: ['CMD', 'pg_isready', '-U', 'affine', '-d', 'affine']
      interval: 10s
      timeout: 5s
      retries: 5
    restart: unless-stopped

มีความแตกต่าง 4 ประการจากไฟล์ที่ต้นทางจัดเตรียมมา และแต่ละจุดมีเหตุผลรองรับ:

  • 127.0.0.1:3010:3010 จะเปิดพอร์ตเฉพาะบน loopback address เท่านั้น เพื่อไม่ให้บุคคลภายนอกเข้าถึง AFFiNE ได้จนกว่าคุณจะตัดสินใจเลือกวิธีเข้าถึงที่เหมาะสม ในขณะที่ไฟล์ '3010:3010' ของต้นทางจะ bind ทุก interface ซึ่งบน VPS ส่วนใหญ่จะรวมถึง public interface ด้วย
  • POSTGRES_HOST_AUTH_METHOD: trust ถูกนำออกและแทนที่ด้วยการตั้งรหัสผ่าน การใช้ trust authentication จะยอมรับการเชื่อมต่อใดๆ ไปยังฐานข้อมูลในฐานะผู้ใช้ affine โดยไม่ต้องใช้รหัสผ่าน ซึ่งจำกัดอยู่แค่ในเครือข่าย Compose ภายในเท่านั้น วิธีนี้อาจใช้งานได้จนกว่าวันหนึ่งคุณจะเพิ่ม container อื่นเข้ามาในเครือข่ายนั้น หรือเผลอเปิดพอร์ต 5432 ขณะกำลัง debug
  • redis:8-alpine ถูกนำมาใช้แทน redis แบบเปล่าๆ ซึ่งจะ resolve ไปที่ latest ณ เดือนสิงหาคม 2026 นั่นคือ Redis 8 ดังนั้นการระบุเวอร์ชันไว้จะช่วยคงเวอร์ชันหลักที่คุณทดสอบแล้วไว้ และป้องกันไม่ให้ Redis 9 ถูกดึงมาใช้โดยไม่ตั้งใจระหว่างการทำ docker compose pull ในอนาคต
  • pgvector/pgvector:pg16 ยังคงค่าเดิมตามที่ต้นทางกำหนดไว้ ด้วยเหตุผลที่กล่าวไปข้างต้น

POSTGRES_PASSWORD จะถูกอ่านเฉพาะตอนที่ Postgres สร้างไดเรกทอรีข้อมูลเป็นครั้งแรกเท่านั้น สำหรับ instance ที่มีอยู่แล้ว ให้ตั้งรหัสผ่านด้วย docker compose exec postgres psql -U affine -c "ALTER USER affine WITH PASSWORD 'yourpassword'" จากนั้นจึงอัปเดต DATABASE_URL ให้ตรงกัน

การตั้งค่าอยู่ใน config/config.json

AFFiNE จะอ่านการตั้งค่าจาก config/config.json ซึ่งเป็นไดเรกทอรีที่คุณ mount ไว้ที่ /root/.affine/config เนื่องจากไม่มีกระบวนการใดสร้างไฟล์ดังกล่าวให้ คุณจึงต้องเขียนไฟล์นี้ขึ้นมาก่อนการเริ่มทำงานครั้งแรก ให้เปิด ~/affine/config/config.json ด้วยโปรแกรมแก้ไขข้อความและใส่เนื้อหาต่อไปนี้ โดยเปลี่ยนโดเมนตัวอย่างให้เป็นโดเมนของคุณ:

{
  "$schema": "https://github.com/toeverything/affine/releases/latest/download/config.schema.json",
  "server": {
    "name": "Team workspace",
    "externalUrl": "https://affine.example.com"
  },
  "copilot": {
    "enabled": false,
    "byok": {
      "enabled": false
    }
  }
}

server.externalUrl จะต้องเป็นที่อยู่ที่ผู้ใช้ของคุณใช้เปิดผ่านเบราว์เซอร์จริงๆ AFFiNE จะสร้างลิงก์แชร์และคำเชิญเข้าสู่ workspace จากค่านี้ ดังนั้นหากปล่อยไว้เป็น http://localhost:3010 คำเชิญที่คุณส่งไปจะชี้ไปยังเครื่องของผู้รับเองและทำให้ใช้งานไม่ได้ ให้ตั้งค่าเป็นที่อยู่ HTTPS สาธารณะก่อนการเริ่มทำงานครั้งแรก เพื่อให้ไฟล์และแผงควบคุมผู้ดูแลระบบมีค่าตรงกันเสมอ

copilot ใช้ควบคุมฟีเจอร์ AI ส่วน copilot.byok.enabled คือสวิตช์สำหรับนำคีย์มาใส่เอง (bring-your-own-key) ซึ่งช่วยให้เจ้าของ workspace สามารถวางคีย์ของผู้ให้บริการโมเดลของตนเองลงในการตั้งค่า workspace ได้ การ self-host AFFiNE จะไม่รวมการสมัครสมาชิก AI ไว้ด้วย หากคุณไม่ต้องการใช้งาน ให้ปล่อยค่าทั้งสองเป็น false

เริ่มการทำงานของ stack:

docker compose up -d
docker compose ps

docker compose ps ควรแสดงรายการ affine_postgres และ affine_redis ว่าอยู่ในสถานะ healthy, affine_server อยู่ในสถานะ running และ affine_migration_job อยู่ในสถานะ exited (0) หากงาน migration มี exit code อื่น ให้ตรวจสอบที่จุดนั้น โดย log จะระบุขั้นตอนที่หยุดทำงาน:

docker compose logs affine_migration

ตรึงอิมเมจไว้ก่อนที่คุณจะลืม

stable เป็นแท็กที่มีการเปลี่ยนแปลงอยู่ตลอด เวิร์กโฟลว์การปล่อยซอฟต์แวร์ของ AFFiNE จะชี้แท็กหลายรายการไปยัง build ที่เสถียรแต่ละตัว โดยมีสองแท็กที่สำคัญในที่นี้คือ stable ซึ่งจะถูกเปลี่ยนจุดชี้ทุกครั้งที่มีการปล่อยเวอร์ชันใหม่ และ stable- ตามด้วย git short hash ซึ่งจะไม่มีการเปลี่ยนแปลง หากคุณปล่อยไว้ที่ stable การทำ docker compose pull ในอีกหกเดือนข้างหน้าจะไปดึงอิมเมจตัวอื่นมาใช้งาน และรันการย้ายฐานข้อมูล (migration) ในเวลาที่คุณไม่ได้เป็นคนกำหนดเอง คุณควรตรึงอิมเมจตัวที่คุณได้ทดสอบแล้วไว้:

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

คำสั่งดังกล่าวจะแสดงบรรทัดที่คล้ายกับ ghcr.io/toeverything/affine@sha256: ตามด้วยค่าแฮชยาวๆ ให้คัดลอกสตริงทั้งหมดไปวางในบรรทัด image: ของทั้ง affine และ affine_migration ทั้งสองไฟล์นี้ต้องตรงกันเสมอเนื่องจากเป็นอิมเมจเดียวกันที่ทำหน้าที่สองอย่าง หากไม่ตรงกันจะส่งผลให้เกิดการย้ายฐานข้อมูลไปยังสคีมาหนึ่ง แต่กลับให้บริการด้วยอีกสคีมาหนึ่ง การอัปเกรดจึงควรเป็นการแก้ไขที่ตั้งใจทำ ไม่ใช่เรื่องเซอร์ไพรส์: ให้เปลี่ยนค่า digest, สำรองข้อมูล, docker compose pull, docker compose up -d

สร้างบัญชีผู้ดูแลระบบก่อนที่ผู้อื่นจะทำได้

เปิด /admin บนอินสแตนซ์ที่เพิ่งติดตั้งใหม่ แล้ว AFFiNE จะนำคุณไปยังหน้าสร้างบัญชี เนื่องจากเซิร์ฟเวอร์ยังไม่มีผู้ดูแลระบบ ในขั้นตอนดังกล่าวไม่มีรหัสเชิญหรือโทเค็นสำหรับการตั้งค่า บุคคลแรกที่โหลดหน้านั้นจะกลายเป็นผู้ดูแลระบบของเซิร์ฟเวอร์คุณ ดังนั้นจึงต้องปิดพอร์ตไว้จนกว่าคุณจะลงทะเบียนเสร็จสิ้น

นั่นคือเหตุผลที่ไฟล์ compose ด้านบนผูกไว้กับ 127.0.0.1 ให้เข้าถึงผ่าน SSH tunnel จากเครื่องของคุณเอง:

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

ปล่อยให้คำสั่งนั้นทำงานอยู่ แล้วเปิด http://127.0.0.1:3010/admin ในเบราว์เซอร์บนเครื่องของคุณ ลงทะเบียนและเข้าสู่ระบบให้เรียบร้อย จากนั้นจึงปิด tunnel เมื่อถึงตอนนี้เท่านั้นจึงจะปลอดภัยที่จะนำอินสแตนซ์ไปใช้งานบนชื่อโดเมนสาธารณะ

ตำแหน่งที่ AFFiNE จัดเก็บข้อมูลของคุณ

ข้อมูลทั้งหมดจะถูกจัดเก็บไว้ใน 3 เส้นทาง ซึ่งทั้งหมดจะอยู่ภายในไดเรกทอรีที่คุณสร้างขึ้น

  • ./data/postgres คือไดเรกทอรีข้อมูลของ Postgres ซึ่งประกอบด้วยเอกสาร, ผู้ใช้งาน, พื้นที่ทำงาน และสิทธิ์การเข้าถึง
  • ./data/storage จะถูก mount ไว้ที่ /root/.affine/storage ภายในคอนเทนเนอร์ และใช้สำหรับเก็บไฟล์ที่อัปโหลดทั้งหมด
  • ./config จะถูก mount ไว้ที่ /root/.affine/config และใช้สำหรับเก็บ config.json

ต้นทางเลือกใช้ bind mounts แทนที่จะเป็น named volumes ซึ่งเป็นการตัดสินใจที่ตั้งใจไว้ เพื่อให้คุณสามารถใช้คำสั่งทั่วไปในการ tar และคัดลอกเส้นทางเหล่านี้ได้โดยไม่ต้องตรวจสอบกับ Docker ว่าจัดเก็บไฟล์ไว้ที่ใด ข้อแลกเปลี่ยนคือคุณต้องเป็นผู้จัดการสิทธิ์ความเป็นเจ้าของไฟล์บนโฮสต์ด้วยตนเอง ซึ่งเป็นประเด็นที่อธิบายไว้ใน bind mounts และ named volumes

วิธีการสำรองข้อมูล AFFiNE

คุณต้องสำรองข้อมูลสองส่วนโดยใช้วิธีที่แตกต่างกัน ฐานข้อมูลเป็นเซิร์ฟเวอร์ที่ทำงานอยู่ตลอดเวลา การคัดลอกไฟล์ขณะที่ฐานข้อมูลกำลังทำงานจะทำให้ได้ไฟล์ที่เสียหาย ดังนั้นให้ใช้วิธีการ dump ข้อมูลแทน:

mkdir -p ~/affine/backup
cd ~/affine
docker compose exec -T postgres pg_dump --format c --username affine affine \
  > backup/affine-$(date +%F).dump
ls -lh backup/

การ dump ข้อมูลจะทำงานภายใน container ผ่าน local socket จึงไม่ต้องระบุรหัสผ่าน ให้ตรวจสอบขนาดไฟล์ในผลลัพธ์ของ ls หากไฟล์มีขนาดเพียงไม่กี่ร้อยไบต์ แสดงว่าการ dump ล้มเหลวแม้ว่า shell จะสร้างไฟล์ขึ้นมาแล้วก็ตาม ซึ่งเป็นความผิดพลาดที่ผู้ใช้มักจะพบหลังจากผ่านไปแล้ว 6 เดือน ส่วน -T ก็มีความสำคัญเช่นกัน หากไม่มี flag นี้ Compose อาจจัดสรร terminal และทำให้ binary stream เสียหายได้

ไฟล์ที่อัปโหลดเป็นเพียงไฟล์ทั่วไป ให้ใช้ tar ในการสำรองข้อมูล:

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

คุณต้องสำรองข้อมูล config.json ด้วยตนเอง เอกสารของ AFFiNE ระบุว่าฟังก์ชันการส่งออกการตั้งค่าจากแผงควบคุมผู้ดูแลระบบยังไม่ถูกพัฒนา (ตรวจสอบเมื่อเดือนสิงหาคม 2026) ดังนั้นไฟล์ที่อยู่บนดิสก์จึงเป็นสำเนาเดียวของการตั้งค่าของคุณ ให้คัดลอกไฟล์ทั้งสามชุดออกจากเซิร์ฟเวอร์ การเก็บข้อมูลสำรองไว้บนดิสก์ลูกเดียวกันกับข้อมูลต้นฉบับไม่ถือว่าเป็นการสำรองข้อมูล

การกู้คืนข้อมูลและข้อควรระวังในขั้นตอนที่เผยแพร่

โปรดอ่านขั้นตอนการกู้คืนข้อมูลอย่างเป็นทางการก่อนที่คุณจะจำเป็นต้องใช้งานจริง และอ่านอย่างละเอียด ในขั้นตอนที่เผยแพร่เมื่อเดือนสิงหาคม 2026 มีการคัดลอกไฟล์ชื่อ affine.backup เข้าไปในคอนเทนเนอร์แล้วจึงกู้คืนจาก ./pg.backup ซึ่งเป็นชื่อไฟล์ที่แตกต่างกัน นอกจากนี้ยังมีการลบไดเรกทอรี ./postgres ทิ้ง ในขณะที่ไฟล์ compose ปัจจุบันเก็บข้อมูลไว้ใน ./data/postgres ให้คุณใช้ path ที่คุณใช้งานจริงแทน path ที่ระบุในตัวอย่าง นี่คือลำดับขั้นตอนที่สอดคล้องกับโครงสร้างในคู่มือฉบับนี้:

cd ~/affine
docker compose down
sudo mv data/postgres data/postgres.old
docker compose up -d postgres
docker compose cp backup/affine-2026-08-08.dump postgres:/tmp/affine.dump
docker compose exec postgres pg_restore --format c --username affine \
  --dbname affine --verbose /tmp/affine.dump
docker compose up -d

โปรดสังเกตการใช้ mv แทนที่จะเป็น rm การกู้คืนข้อมูลทับลงบนฐานข้อมูลที่คุณไม่ได้สำรองไว้คือสาเหตุที่ทำให้คำสั่งที่ผิดพลาดเพียงคำสั่งเดียวกลายเป็นการสูญเสียข้อมูลทั้งหมด การย้ายไดเรกทอรีเก่าออกไปไว้ที่อื่นไม่มีค่าใช้จ่ายใดๆ ให้กู้คืนไฟล์ที่อัปโหลดด้วย tar xzf backup/storage-2026-08-08.tgz -C data เช่นกัน มิฉะนั้นเอกสารทุกฉบับจะแสดงผลโดยไม่มีไฟล์แนบ จากนั้นให้เข้าสู่ระบบและเปิดเอกสารที่มีรูปภาพประกอบ นั่นคือการทดสอบ การกู้คืนข้อมูลที่คุณไม่ได้เปิดตรวจสอบผ่านเบราว์เซอร์นั้นเป็นเพียงแค่ไฟล์ ไม่ใช่การสำรองข้อมูลที่สมบูรณ์

การวาง AFFiNE ไว้หลัง Proxy ที่คุณใช้งานอยู่แล้ว

AFFiNE จำเป็นต้องใช้ WebSocket โดยไม่มีข้อยกเว้น เอกสารระบุไว้อย่างชัดเจนว่า WebSocket เป็นพื้นฐานของระบบซิงค์และการทำงานร่วมกันของ AFFiNE ดังนั้นหาก Proxy ไม่รองรับการอัปเกรดการเชื่อมต่อดังกล่าว จะส่งผลให้ Workspace หยุดการซิงค์ข้อมูลโดยไม่มีการแจ้งเตือน หน้าเว็บจะโหลดได้ปกติ การล็อกอินใช้งานได้ แต่การแก้ไขในเบราว์เซอร์หนึ่งจะไม่ไปปรากฏในอีกเบราว์เซอร์หนึ่ง ให้เปิดเครื่องมือสำหรับนักพัฒนา (Developer Tools) ในเบราว์เซอร์ของคุณ ไปที่แท็บ Network แล้วกรองเฉพาะ WS หากพบการเชื่อมต่อที่เปิดและปิดซ้ำๆ แสดงว่า Proxy ของคุณไม่ได้ส่งผ่านการอัปเกรด (Upgrade)

หากคุณใช้งาน Traefik สำหรับคอนเทนเนอร์อื่นอยู่แล้ว คุณสามารถเพิ่ม AFFiNE เข้าไปเป็นบริการปกติได้ ให้ลบส่วน ports: ออกจากบริการ affine แล้วเพิ่มส่วนนี้แทน:

    networks:
      - default
      - proxy
    labels:
      - 'traefik.enable=true'
      - 'traefik.docker.network=proxy'
      - 'traefik.http.routers.affine.rule=Host(`affine.example.com`)'
      - 'traefik.http.routers.affine.entrypoints=websecure'
      - 'traefik.http.routers.affine.tls.certresolver=letsencrypt'
      - 'traefik.http.services.affine.loadbalancer.server.port=3010'

และที่ด้านล่างของไฟล์ ในระดับเดียวกับ services: ให้เพิ่ม:

networks:
  proxy:
    external: true

ชื่อของ certificate resolver ต้องตรงกับที่กำหนดไว้ในคอนฟิกูเรชันของ Traefik และ loadbalancer.server.port คือพอร์ตภายในคอนเทนเนอร์ซึ่งก็คือ 3010 ไม่ใช่พอร์ตของโฮสต์ Traefik จะทำหน้าที่ Proxy การเชื่อมต่อ WebSocket โดยอัตโนมัติโดยไม่ต้องตั้งค่าเพิ่มเติม หากบริการส่วนที่เหลือของคุณอยู่หลัง Authentik สำหรับ single sign-on คุณสามารถใช้ forward auth middleware บนเราเตอร์นี้เพื่อจำกัดการเข้าถึง AFFiNE ผ่านเบราว์เซอร์ได้ แต่แนะนำให้ปิดไว้ก่อนจนกว่าจะทดสอบแอปบนเดสก์ท็อปเสร็จสิ้น เนื่องจากแอปเดสก์ท็อปไม่มี session ของเบราว์เซอร์และจะทำให้การซิงค์ล้มเหลวทันที การรันหลายแอปหลัง Proxy ตัวเดียวได้อธิบายไว้ใน การใช้ Traefik ตัวเดียวหน้าหลายแอป

สำหรับ nginx คุณต้องระบุการอัปเกรดอย่างชัดเจน:

location / {
    proxy_pass http://127.0.0.1:3010;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    client_max_body_size 100m;
}

ค่าเริ่มต้นของ client_max_body_size ใน nginx คือ 1 MB ดังนั้นหากไม่มีบรรทัดดังกล่าว การอัปโหลดไฟล์ที่ใหญ่กว่ารูปภาพขนาดเล็กจะล้มเหลวด้วยสถานะ 413 และจะไม่ปรากฏข้อความใดๆ ใน log ของ AFFiNE เนื่องจากคำขอดังกล่าวไม่เคยถูกส่งไปถึง สำหรับ Caddy คุณเพียงแค่เพิ่มบรรทัด reverse_proxy http://127.0.0.1:3010 ซึ่งจะจัดการเรื่อง certificate และการอัปเกรด WebSocket ให้โดยอัตโนมัติ

สิ่งที่การติดตั้งแบบ self-hosted ยังขาดไป

จงซื่อสัตย์กับตัวเองในเรื่องนี้ก่อนที่จะย้ายทีมมาใช้งาน

ฟีเจอร์การทำงานร่วมกันแบบเรียลไทม์นั้นมีอยู่จริง และเป็นฟีเจอร์หลักที่คำแนะนำเรื่องขนาดของระบบอ้างถึง เนื่องจากเอกสารของ AFFiNE ระบุว่าการใช้หน่วยความจำขึ้นอยู่กับระบบซิงค์และการรวมเอกสาร (document merging) การแก้ไขแบบออฟไลน์เป็นเหตุผลที่หลายคนต้องการเครื่องมือแบบ local-first และแอปพลิเคชันบนเดสก์ท็อปสามารถเพิ่มเซิร์ฟเวอร์ที่คุณติดตั้งเองเข้าไปในรายการ workspace และล็อกอินเข้าใช้งานได้ ให้ทดสอบพฤติกรรมแบบออฟไลน์ที่ทีมของคุณต้องใช้งานจริงให้ละเอียดก่อนตัดสินใจใช้งาน: ลองแก้ไขในแอปเดสก์ท็อปขณะปิดเครือข่าย จากนั้นเชื่อมต่อใหม่ แล้วตรวจสอบผลลัพธ์บนอุปกรณ์เครื่องที่สอง รายการฟีเจอร์ไม่ใช่หลักฐานยืนยัน และนั่นรวมถึงรายการนี้ด้วย

ระบบค้นหาข้อความแบบเต็ม (full-text search) ฝั่งเซิร์ฟเวอร์ถูกปิดไว้ในไฟล์ compose ที่แจกมา โดยมีการตั้งค่า AFFINE_INDEXER_ENABLED=false ไว้ทั้งบนเซิร์ฟเวอร์และบนงาน migration การเปิดใช้งานหมายถึงการเพิ่มคอนเทนเนอร์ Manticore Search ซึ่งเป็นบริการที่ห้าและใช้หน่วยความจำเพิ่มขึ้น บนเครื่องขนาด 2 GB การเปลี่ยนแปลงนี้คือจุดที่จะทำให้ทรัพยากรระบบไม่เพียงพอ การค้นหาภายในไคลเอนต์ยังคงใช้งานได้กับ workspace ที่คุณเปิดอยู่

มีข้อจำกัดสองประการที่ควรทราบก่อนเชิญผู้อื่นเข้ามาใช้งาน: workspace แบบ self-hosted รองรับผู้ใช้สูงสุด 10 ที่นั่ง และหากต้องการมากกว่านั้นจำเป็นต้องมีใบอนุญาต Team จาก AFFiNE ส่วนการจัดเก็บ blob แบบไม่จำกัดและขนาด blob แบบไม่จำกัดสำหรับอินสแตนซ์แบบ self-hosted นั้น เอกสารระบุว่ามีแผนจะทำแต่ยังไม่ได้ดำเนินการอย่างสมบูรณ์ ณ เดือนสิงหาคม 2026 ทั้งสองประเด็นนี้ไม่มีผลกับครัวเรือนหรือทีมขนาดเล็ก แต่จะมีผลทันทีหากคุณวางแผนที่จะย้ายคนจำนวน 40 คนเข้ามาใช้งาน

การอัปเกรด

โปรดอ่านบันทึกประจำรุ่น (release notes) ก่อนเสมอ โดยเฉพาะอย่างยิ่งสำหรับการอัปเกรดเวอร์ชันย่อย เช่น จาก 0.26 ไปเป็น 0.27 ซึ่งมักมีการเปลี่ยนแปลงที่อาจส่งผลกระทบต่อระบบ (breaking changes) ให้สำรองข้อมูลฐานข้อมูลและไดเรกทอรีจัดเก็บข้อมูลก่อนดำเนินการใดๆ เนื่องจากกระบวนการย้ายข้อมูล (migration) จะปรับเปลี่ยนโครงสร้างฐานข้อมูล (schema) ในการเริ่มระบบครั้งถัดไปและไม่สามารถย้อนกลับได้ จากนั้นให้เปลี่ยนค่า digest ที่ระบุไว้ แล้วรัน docker compose pull ตามด้วย docker compose up -d และเฝ้าดู docker compose logs -f affine_migration จนกว่ากระบวนการจะเสร็จสิ้นโดยสมบูรณ์ ทั้งนี้ docker image prune จะทำหน้าที่ล้างข้อมูลเลเยอร์เก่าออกหลังจากนั้น สำหรับผู้ที่ใช้งานเวอร์ชันเก่ามาก มีข้อควรทราบทางประวัติศาสตร์คือ ตั้งแต่เวอร์ชัน 0.23.0 เป็นต้นมา ชื่ออิมเมจได้เปลี่ยนจาก affine-graphql เป็น affine ดังนั้นไฟล์ compose ที่เก่ากว่านั้นจำเป็นต้องแก้ไขบรรทัด image ก่อนที่จะสามารถดึงอิมเมจใหม่ได้สำเร็จ

FAQ

ทำไมคอนเทนเนอร์ AFFiNE ถึงไม่เริ่มทำงาน?

บริการ affine มีการประกาศ condition: service_completed_successfully ไว้บนงาน affine_migration ดังนั้นหากกระบวนการ migration จบลงด้วยสถานะอื่นที่ไม่ใช่ 0 เซิร์ฟเวอร์จะไม่เริ่มทำงานและไม่มีหน้าเว็บปรากฏขึ้น ให้รันคำสั่ง docker compose logs affine_migration เพื่อตรวจสอบว่าขั้นตอนใดที่หยุดทำงาน สาเหตุที่พบบ่อยที่สุดในไฟล์ compose ที่แก้ไขด้วยตนเองคือการใช้ image postgres แบบมาตรฐานแทนที่จะเป็น pgvector/pgvector:pg16 เนื่องจาก schema ของ AFFiNE มีการประกาศส่วนขยาย pgvector และสร้างตารางที่มีคอลัมน์ vector(1024) ซึ่ง Postgres รุ่นปกติไม่สามารถสร้างได้

AFFiNE ที่โฮสต์เองต้องใช้ RAM เท่าไร?

หน้าความต้องการของระบบของ AFFiNE ระบุว่าต้องใช้ CPU อย่างน้อย 4 คอร์และ RAM 2 GB และจะเพิ่มเป็น 4 GB เมื่อเอกสารมีจำนวนคำเกิน 10,000 คำ นอกจากนี้ยังระบุว่าการรวมเอกสารที่มีการแก้ไข 10,000 รายการอาจใช้ RAM สูงสุดถึง 1 GB บนเซิร์ฟเวอร์ขนาด 2 GB ปริมาณการใช้งานสูงสุดนี้เองที่เป็นสาเหตุให้ระบบล่ม ไม่ใช่ภาระงานขณะไม่ได้ใช้งาน โดย kernel out-of-memory killer จะหยุดกระบวนการ AFFiNE และ restart: unless-stopped จะเริ่มการทำงานใหม่ ทำให้ผู้ใช้เห็นหน้าเว็บโหลดซ้ำแทนที่จะเห็นข้อความแสดงข้อผิดพลาด ให้ยืนยันปัญหานี้ด้วย docker inspect affine_server --format '{{.State.OOMKilled}}' และ sudo dmesg -T | grep -i 'out of memory' จากนั้นให้เพิ่ม swap file ขนาด 2 GB เพื่อให้ช่วงที่ระบบมีการใช้งานสูงทำงานช้าลงแทนที่จะหยุดทำงานทันที

AFFiNE เก็บข้อมูลของฉันไว้ที่ไหน และต้องสำรองข้อมูลอะไรบ้าง?

ข้อมูลทั้งหมดจะถูกเก็บไว้ใน 3 พาธภายใต้ไดเรกทอรี compose ของคุณ ได้แก่ ./data/postgres สำหรับฐานข้อมูล, ./data/storage สำหรับไฟล์ที่อัปโหลด และ ./config สำหรับ config.json ให้สำรองข้อมูลฐานข้อมูลด้วย docker compose exec -T postgres pg_dump --format c --username affine affine > affine.dump แทนการคัดลอกไฟล์โดยตรง เนื่องจากไม่สามารถคัดลอกไฟล์ของ Postgres ที่กำลังทำงานอยู่ได้อย่างปลอดภัย ให้ใช้คำสั่ง tar กับ ./data/storage สำหรับไฟล์ที่อัปโหลด และเก็บสำเนาของ config.json ไว้ด้วยตนเอง เนื่องจากฟังก์ชันการส่งออกการตั้งค่าจากแผงควบคุมผู้ดูแลระบบยังไม่ถูกพัฒนาขึ้น ณ เดือนสิงหาคม 2026

การทำงานร่วมกันแบบเรียลไทม์ใช้งานได้บน AFFiNE ที่โฮสต์เองหรือไม่?

ได้ และไม่จำเป็นต้องตั้งค่าใดๆ เพิ่มเติม ข้อกำหนดเพียงอย่างเดียวคือ reverse proxy ของคุณ เนื่องจากระบบซิงค์ทำงานผ่านการเชื่อมต่อ WebSocket สำหรับ nginx หมายความว่าต้องตั้งค่า proxy_http_version 1.1 รวมถึง header Upgrade และ Connection: upgrade ในขณะที่ Traefik และ Caddy จะส่งผ่านการเชื่อมต่อเหล่านี้โดยไม่ต้องตั้งค่าเพิ่มเติม อาการของ proxy ที่ไม่ได้อัปเกรดการเชื่อมต่อคือ workspace จะโหลดและล็อกอินได้ตามปกติ แต่การแก้ไขที่ทำในเบราว์เซอร์หนึ่งจะไม่ปรากฏในอีกเบราว์เซอร์หนึ่ง

ฉันสามารถรัน AFFiNE ด้วย image Postgres มาตรฐานได้หรือไม่?

ไม่ได้ schema.prisma ของ AFFiNE มีการประกาศ extensions = [pgvector(map: "vector")] และกำหนดตาราง 4 ตารางที่มีคอลัมน์ embedding เป็นชนิด vector(1024) ซึ่งงาน migration จะสร้างตารางเหล่านั้นแม้ว่าจะปิดฟีเจอร์ AI ไว้ก็ตาม ให้ใช้ pgvector/pgvector:pg16 ซึ่งเป็น Postgres 16 ที่คอมไพล์ส่วนขยายดังกล่าวมาให้แล้ว หากคุณชี้ AFFiNE ไปยังเซิร์ฟเวอร์ Postgres ภายนอก ให้ติดตั้ง pgvector บนเซิร์ฟเวอร์นั้นและสร้างส่วนขยายในฐานข้อมูลเป้าหมายก่อนที่จะรันการ migration