SSD Nodes Learn Hosting plans →
คู่มือ Matt Connorโดย Matt Connor

วิธีติดตั้ง mem0 บน VPS เพื่อทำระบบความจำให้ AI Agent

เรียนรู้วิธีติดตั้ง mem0 บน VPS ของคุณเอง พร้อมการตั้งค่า Docker Compose, การทำ TLS และการเชื่อมต่อ Ollama แบบ local โดยใช้ RAM ขั้นต่ำ 8GB สำหรับการรันแบบครบวงจร

ต้นทุนการใช้ RAM จริงสำหรับการ self-host mem0 บน VPS

การ self-host mem0 หมายถึงการรันคอนเทนเนอร์ 3 รายการ ได้แก่ เซิร์ฟเวอร์ FastAPI สำหรับจัดการหน่วยความจำ, Postgres ที่ติดตั้งส่วนขยาย pgvector และแดชบอร์ด Next.js โดย mem0 ทำหน้าที่เป็นเลเยอร์หน่วยความจำสำหรับเอเจนต์ เมื่อคุณส่งบทสนทนาเข้าไป โมเดลภาษาจะดึงข้อเท็จจริงที่สำคัญออกมาจากบทสนทนานั้น แล้วจัดเก็บข้อเท็จจริงเหล่านั้นในรูปแบบเวกเตอร์ เพื่อให้การสืบค้นในภายหลังสามารถดึงข้อมูลที่เกี่ยวข้องกลับมาได้

ให้เตรียมงบประมาณ RAM ไว้ประมาณ 1 GB สำหรับคอนเทนเนอร์ทั้งสามรายการ และพื้นที่ดิสก์ 3 ถึง 4 GB หลังจาก build image เรียบร้อยแล้ว VPS ขนาด 2 GB สามารถรันงานนี้ได้อย่างสบายหากโมเดลภาษาถูกรันไว้ที่อื่น แต่เมื่อรันโมเดลบนเครื่องเดียวกันผ่าน Ollama ตัวโมเดลจะใช้ทรัพยากรหลักทั้งหมด โดยโมเดลขนาด 8B ที่ถูกทำ quantization เหลือ 4 bits จะต้องการ RAM ประมาณ 6 GB ดังนั้นการติดตั้งแบบ local ทั้งหมดจึงต้องใช้ RAM เริ่มต้นที่ 8 GB

อย่าเชื่อตัวเลขเหล่านี้จากโพสต์บล็อกเพียงอย่างเดียว รวมถึงโพสต์นี้ด้วย ให้ทำการวัดค่าจาก stack ที่คุณสร้างขึ้นจริง

docker compose ps
docker stats --no-stream
docker system df -v

docker stats จะแสดงค่า resident memory ของแต่ละคอนเทนเนอร์ ส่วน docker system df -v จะแสดงขนาดดิสก์ที่แต่ละ image และ volume ใช้งานอยู่

สถานะการทำงานปกติไม่ใช่ช่วงที่ใช้ทรัพยากรสูงสุด docker compose up -d --build จะทำการ compile แดชบอร์ด Next.js ซึ่งขั้นตอนการ build ด้วย Node นี้เป็นช่วงที่ใช้ทรัพยากรมากที่สุดของการติดตั้งทั้งหมด บน VPS ขนาด 1 GB ตัว kernel out-of-memory killer จะหยุดการทำงานดังกล่าวและทำให้การ build จบลงด้วย exit code 137 ให้ตรวจสอบสาเหตุให้แน่ชัดก่อนจะสรุปว่าเป็นบั๊กของ Docker:

dmesg -T | grep -i "killed process"

หากเซิร์ฟเวอร์ดูจะเป็นภาระเกินความจำเป็นสำหรับสิ่งที่คุณต้องการ ยังมีทางเลือกขนาดเล็กกว่าที่ใช้งานได้จริง ทั้ง local agent memory store ที่ไม่ต้องใช้เซิร์ฟเวอร์เลย และ หน่วยความจำที่อยู่ในตัว Claude Code เอง ซึ่งทั้งสองวิธีนี้ไม่ต้องใช้ฐานข้อมูล คุณสามารถกลับมาพิจารณาวิธีนี้ได้เมื่อเอเจนต์หลายตัวหรือเครื่องหลายเครื่องจำเป็นต้องอ่านหน่วยความจำชุดเดียวกัน

ฉันจำเป็นต้องใช้ Neo4j สำหรับ graph memory ของ mem0 หรือไม่

ไม่จำเป็น หากคู่มือใดแนะนำให้คุณเพิ่มคอนเทนเนอร์ Neo4j คู่มือนั้นถือว่าล้าสมัยกว่าตัวซอร์สโค้ดปัจจุบัน

ในอดีต graph memory ใน mem0 หมายถึงการใช้ฐานข้อมูลกราฟภายนอก โดยกำหนดค่าภายใต้คีย์ graph_store และตั้งค่า enable_graph เป็น true อัลกอริทึมหน่วยความจำแบบใหม่ที่ปล่อยออกมาในเดือนเมษายน 2026 ได้ตัดคีย์ทั้งสองนี้ออกจาก SDK แบบโอเพนซอร์สแล้ว ปัจจุบันการสกัดเอนทิตี (entity extraction) จะทำงานภายในขั้นตอนการเพิ่มข้อมูลปกติ และเอนทิตีเหล่านั้นจะถูกเขียนลงใน pgvector collection ชุดที่สองซึ่งตั้งชื่อตาม collection หลักของคุณโดยมี _entities ต่อท้าย ไม่จำเป็นต้องมีขั้นตอนการตั้งค่าใดๆ เนื่องจาก collection ดังกล่าวจะถูกสร้างขึ้นเมื่อมีการใช้งานครั้งแรก ดังนั้นฟีเจอร์การเชื่อมโยงเอนทิตีในตัวจะเริ่มทำงานทันทีในการเรียกใช้คำสั่ง add ครั้งถัดไป

การยกเลิกการใช้ graph store ช่วยประหยัดทรัพยากรจากการไม่ต้องรันคอนเทนเนอร์ JVM รวมถึงหน่วยความจำ heap และขนาดอิมเมจอีกหลายร้อยเมกะไบต์ บน VPS ขนาด 2 GB นี่คือความแตกต่างระหว่างการรันโปรแกรมได้ตามปกติกับการเกิด swap

นี่คือสิ่งที่คุณจะสูญเสียไปหากอธิบายตามตรง: ผลลัพธ์การค้นหาเคยมีฟิลด์ relations ที่แสดงรายการความสัมพันธ์ (edges) ระหว่างเอนทิตี ซึ่งฟิลด์ดังกล่าวได้ถูกตัดออกไปแล้ว ปัจจุบันการจับคู่เอนทิตีจะส่งผลให้ลำดับความสำคัญของหน่วยความจำในคะแนนรวมสูงขึ้น และไม่มีโครงสร้างที่คุณจะสามารถไล่เรียงความสัมพันธ์ได้อีกต่อไป หากแอปพลิเคชันของคุณเคยใช้การไล่เรียงความสัมพันธ์เหล่านั้น mem0 จะไม่เก็บข้อมูลดังกล่าวไว้อีกต่อไป และคุณจะต้องดูแลฐานข้อมูลกราฟของคุณเองแยกต่างหากจาก mem0 โดยป้อนข้อมูลผ่านโค้ดของคุณเอง

ไฟล์ compose ใน repository นี้เป็นไฟล์สำหรับพัฒนา

server/docker-compose.yaml ประกาศ name: mem0-dev และมันมีความหมายตามนั้น โปรดอ่านไฟล์ก่อนเรียกใช้งาน เพราะมี 5 จุดที่ผิดพลาดสำหรับการนำไปใช้บนเซิร์ฟเวอร์

  • มันทำการ build จาก server/dev.Dockerfile และ mount ไดเรกทอรีที่คุณ checkout ไว้ทับลงบน image ด้วย .:/app ดังนั้น container จะรันสิ่งที่อยู่ในไดเรกทอรีนั้นแทนที่จะเป็นสิ่งที่ถูก build ไว้
  • คำสั่งของมันคือ rm -rf /app/packages && pip install -q --force-reinstall --no-deps mem0ai && alembic upgrade head && uvicorn main:app --host 0.0.0.0 --port 8000 --reload ซึ่งจะติดตั้ง mem0ai จาก PyPI ใหม่ทุกครั้งที่เริ่มทำงาน ดังนั้นเวอร์ชันที่เซิร์ฟเวอร์ของคุณรันอยู่อาจเปลี่ยนไปในระหว่างการ restart ที่คุณไม่ได้ตั้งใจจะอัปเกรด
  • ขั้นตอน pip เดียวกันนี้หมายความว่า หากการ restart ครั้งใดไม่มีการเชื่อมต่อเครือข่ายภายนอก กระบวนการจะล้มเหลวก่อนที่ uvicorn จะเริ่มทำงาน ส่งผลให้เซิร์ฟเวอร์หน่วยความจำของคุณหยุดทำงานเพียงเพราะไม่สามารถเข้าถึง PyPI ได้
  • --reload จะเริ่มการทำงานของตัวเฝ้าติดตามไฟล์ (file watcher) ของ uvicorn ซึ่งมีไว้เพื่อ restart กระบวนการเมื่อคุณแก้ไขโค้ด แต่มันสิ้นเปลืองหน่วยความจำและสร้างกระบวนการที่สองขึ้นมาโดยไม่มีประโยชน์ในสภาพแวดล้อม production นอกจากนี้ Dockerfile ใน production ยังมี --reload อยู่ใน CMD ด้วย ดังนั้นคุณจึงต้อง override คำสั่งไม่ว่าจะด้วยวิธีใดก็ตาม
  • พอร์ตที่เปิดใช้งานคือ "8888:8000", "8432:5432" และ "3000:3000" การเปิดพอร์ตโดยไม่ระบุที่อยู่ด้านหน้าจะทำให้มัน bind เข้ากับ 0.0.0.0 ส่งผลให้ Postgres ตอบสนองต่ออินเทอร์เน็ตสาธารณะผ่านพอร์ต 8432 ทันทีที่ stack เริ่มทำงาน

ประเด็นสุดท้ายนี้ควรค่าแก่การเตือนเป็นพิเศษ Docker จะเปิดพอร์ตโดยการเขียนกฎของตัวเองนำหน้า chain ที่ ufw จัดการอยู่ ดังนั้น ufw deny 8432 จึงไม่สามารถปิดพอร์ตของ container ที่ถูกเปิดใช้งานไว้ได้ การที่ Docker เปิดพอร์ตข้าม ufw จะอธิบายถึงกฎที่เกี่ยวข้องโดยละเอียด

ไฟล์ compose สำหรับเซิร์ฟเวอร์จริง

ทำงานภายใน server/ โดยคง init-db.sh ไว้ที่เดิม และแทนที่ docker-compose.yaml ด้วยเนื้อหาต่อไปนี้

name: mem0

services:
  mem0:
    build:
      context: .
      dockerfile: Dockerfile
    restart: unless-stopped
    env_file: .env
    ports:
      - "127.0.0.1:8888:8000"
    networks: [mem0_network]
    volumes:
      - mem0_history:/app/history
    depends_on:
      postgres:
        condition: service_healthy
    command: >
      sh -c "alembic upgrade head &&
             uvicorn main:app --host 0.0.0.0 --port 8000"
    environment:
      - PYTHONUNBUFFERED=1
      - DASHBOARD_URL=https://mem0.example.com
      - APP_DB_NAME=mem0_app
      - AUTH_DISABLED=false
      - MEM0_TELEMETRY=false

  postgres:
    image: pgvector/pgvector:pg17
    restart: unless-stopped
    shm_size: "128mb"
    networks: [mem0_network]
    environment:
      - POSTGRES_USER=${POSTGRES_USER:-postgres}
      - POSTGRES_PASSWORD=${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env}
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -q -U ${POSTGRES_USER:-postgres}"]
      interval: 5s
      timeout: 5s
      retries: 5
    volumes:
      - postgres_db:/var/lib/postgresql/data
      - ./init-db.sh:/docker-entrypoint-initdb.d/init-db.sh

  mem0-dashboard:
    build: ./dashboard
    restart: unless-stopped
    ports:
      - "127.0.0.1:3000:3000"
    networks: [mem0_network]
    environment:
      - NEXT_PUBLIC_API_URL=https://mem0.example.com
      - API_INTERNAL_URL=http://mem0:8000
    depends_on:
      mem0:
        condition: service_started

volumes:
  postgres_db:
  mem0_history:

networks:
  mem0_network:
    driver: bridge

มีการเปลี่ยนแปลงสำคัญ 5 ประการที่ต้องพิจารณา โดยแต่ละประการมีเหตุผลรองรับดังนี้

ทุกรายการใน ports จะขึ้นต้นด้วย 127.0.0.1 เพื่อให้ kernel ยอมรับการเชื่อมต่อเหล่านั้นจากภายในเครื่องเท่านั้น การเชื่อมต่อทั้งหมดจากภายนอกจะผ่าน reverse proxy ซึ่งเป็นจุดเดียวที่ถือ certificate ไว้

Postgres ไม่มีบล็อก ports เลย เนื่องจาก container mem0 สามารถเข้าถึงได้ผ่าน mem0_network โดยใช้ชื่อบริการ ดังนั้นการเปิดพอร์ต 8432 จึงไม่มีประโยชน์และเป็นการเปิดพอร์ตทิ้งไว้โดยไม่จำเป็น ให้ใช้ docker compose exec postgres psql -U postgres เมื่อคุณต้องการเข้าถึง shell

ประวัติการใช้งานจะถูกย้ายจาก bind mount ใน ./history ไปเป็น named volume แทน การใช้ bind mount จะผูกข้อมูลไว้กับ path และ uid หนึ่งๆ บนโฮสต์นี้ ในขณะที่ named volume เป็นออบเจกต์ที่ Docker สามารถทำ snapshot และย้ายไปมาได้ Named volumes เทียบกับ bind mounts จะอธิบายว่าควรเลือกใช้แบบใดในสถานการณ์ที่เหมาะสม

คำสั่งจะตัด --reload ออกและคง alembic upgrade head ไว้ โปรดคงขั้นตอนการ migration นี้ไว้ หากไม่มีขั้นตอนนี้ แอปจะเริ่มทำงานโดยเชื่อมต่อกับฐานข้อมูลที่ไม่มีตาราง และทุกคำขอจะล้มเหลวตั้งแต่ query แรก

NEXT_PUBLIC_API_URL คือ URL ที่เบราว์เซอร์ของคุณเรียกใช้งาน ดังนั้นจึงต้องเป็นที่อยู่ HTTPS สาธารณะ ไม่ใช่ http://mem0:8000 โดยปกติ Next.js จะฝังค่า NEXT_PUBLIC_ ไว้ตั้งแต่ขั้นตอน build แต่ image ของ dashboard นี้ใช้ค่า placeholder แทน และ entrypoint จะทำการแทนที่ด้วยค่าจริงของ container เมื่อ container เริ่มทำงาน ดังนั้นการเปลี่ยนแปลงค่านี้จึงจำเป็นต้องใช้ docker compose up -d mem0-dashboard เพื่อสร้าง container ขึ้นใหม่ ไม่ใช่การ rebuild ส่วน docker compose restart mem0-dashboard จะเป็นการนำ container เดิมกลับมาใช้ใหม่ ซึ่งไฟล์ภายในยังคงเก็บค่าเดิมไว้ ทำให้ dashboard ยังคงเรียกไปยัง host ที่ผิดอยู่

ความลับต้องอยู่ใน .env และ .env ต้องไม่ถูกเปิดเผยบนอินเทอร์เน็ต

cd server
cp .env.example .env
openssl rand -hex 32    # paste into JWT_SECRET
openssl rand -hex 32    # paste into ADMIN_API_KEY
chmod 600 .env

ตั้งค่า POSTGRES_PASSWORD, JWT_SECRET และ ADMIN_API_KEY โดยให้เว้น AUTH_DISABLED=false ไว้ ชื่อของ flag นี้บอกชัดเจนว่ามันทำหน้าที่อะไร หากเปิดใช้งาน เซิร์ฟเวอร์จะส่งข้อมูลในหน่วยความจำทั้งหมดที่มีให้กับใครก็ตามที่เข้าถึงพอร์ตนั้นได้ ให้ตั้งค่า MEM0_TELEMETRY=false หากคุณไม่ต้องการให้เหตุการณ์การเริ่มต้นใช้งาน (onboarding event) ถูกส่งไปยัง upstream

ADMIN_API_KEY จะถูกนำไปเปรียบเทียบกับ header X-API-Key โดยใช้ secrets.compare_digest ซึ่งหากตรงกัน ระบบจะข้ามการค้นหาในฐานข้อมูลทั้งหมด นี่คือ credential ระดับ root สำหรับ API ทั้งระบบ ให้ปฏิบัติต่อมันอย่างเคร่งครัด: ห้ามเก็บไว้ใน shell history, ห้ามนำเข้า git และห้ามคัดลอกไปวางใน prompt ทั้ง การเขียนไฟล์ Compose env และจุดที่ความลับรั่วไหลจากไฟล์เหล่านั้น และ การเก็บ API keys ให้พ้นจากบริบทของ agent ต่างมีผลโดยตรงกับกรณีนี้ เนื่องจากผู้เรียกใช้งานเซิร์ฟเวอร์นี้คือ agent

ค่าที่โหลดจาก env_file จะอยู่ใน environment ของ container และ docker inspect จะแสดงค่าเหล่านั้นออกมาทั้งหมด ใครก็ตามที่อยู่ในกลุ่ม docker สามารถอ่านค่าเหล่านี้ได้ และใครก็ตามที่อยู่ในกลุ่ม docker จะมีสิทธิ์เทียบเท่า root บนโฮสต์โดยปริยาย

วาง TLS ไว้หน้า API แทนการเปิดพอร์ต 8888

API ตอบสนองที่ 127.0.0.1:8888 และแดชบอร์ดที่ 127.0.0.1:3000 โดยให้ nginx ทำหน้าที่ terminate TLS (transport layer security) ที่พอร์ต 443 แล้วส่งต่อคำขอไปยังทั้งสองบริการ

server {
    listen 443 ssl;
    server_name mem0.example.com;

    ssl_certificate     /etc/letsencrypt/live/mem0.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/mem0.example.com/privkey.pem;

    location ~ ^/(memories|search|configure|auth|api-keys|docs|openapi.json) {
        proxy_pass http://127.0.0.1:8888;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_read_timeout 180s;
    }

    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}

proxy_read_timeout มีความสำคัญมากกว่าที่เห็น การเรียกใช้งาน add จะค้างไว้ในขณะที่ language model อ่านบทสนทนาและดึงข้อมูลสำคัญออกมา โดยปกติแล้วโมเดลขนาด 8B ที่รันบน CPU ในเครื่องจะใช้เวลาประมวลผลนานกว่าค่าเริ่มต้น 60 วินาทีของ nginx ส่งผลให้ผู้เรียกเห็น 504 Gateway Time-out ในขณะที่โมเดลยังคงทำงานอยู่และหน่วยความจำยังคงถูกเขียนลงไป ผลลัพธ์คือคุณจะได้หน่วยความจำที่ระบบแจ้งว่าบันทึกล้มเหลว

ปิดพอร์ตที่เหลือด้วย นโยบาย default deny ของ ufw โดยเปิดไว้เพียงพอร์ต 22, 80 และ 443 ออกใบรับรองด้วย certbot บน Ubuntu 24.04 หลัง nginx พอร์ต 80 จำเป็นต้องเปิดไว้เนื่องจากขั้นตอนดังกล่าวใช้การตรวจสอบแบบ HTTP-01 challenge ซึ่ง Let's Encrypt จะตรวจสอบผ่านพอร์ต 80 เท่านั้นทั้งในขั้นตอนการออกใบรับรองและการต่ออายุ หากปิดพอร์ต 80 ไว้ คุณจะต้องเปลี่ยนไปใช้การตรวจสอบแบบ DNS-01 challenge แทน หากเซิร์ฟเวอร์ของคุณมีแอปพลิเคชันอื่นที่ใช้งาน Traefik สำหรับ routing หลาย Compose apps อยู่แล้ว ให้เพิ่ม mem0 เข้าไปใน router ตัวนั้นแทนการติดตั้ง proxy ตัวที่สอง

การทดสอบ Smoke test: เพิ่มข้อมูลหน่วยความจำหนึ่งรายการและอ่านกลับ

export MEM0_KEY='<the ADMIN_API_KEY from .env>'

curl -sS -X POST http://127.0.0.1:8888/memories \
  -H "Content-Type: application/json" \
  -H "X-API-Key: $MEM0_KEY" \
  -d '{"messages":[{"role":"user","content":"I deploy with Docker Compose and I run Postgres 17."}],"user_id":"smoke"}'

การตอบสนองที่ถูกต้องคือออบเจกต์ JSON ที่มีรายการ results โดยแต่ละรายการจะประกอบด้วย id, ข้อความ memory ที่ดึงมาได้ และ "event": "ADD" อัลกอริทึมปัจจุบันจะส่งคืนเฉพาะเหตุการณ์ ADD เท่านั้น เหตุการณ์ UPDATE และ DELETE ถูกนำออกไปแล้ว ดังนั้นการที่เหตุการณ์เหล่านี้ไม่ปรากฏจึงไม่ใช่ข้อผิดพลาด

curl -sS -X POST http://127.0.0.1:8888/search \
  -H "Content-Type: application/json" \
  -H "X-API-Key: $MEM0_KEY" \
  -d '{"query":"which database do I run?","filters":{"user_id":"smoke"},"top_k":5}'

ข้อมูลเกี่ยวกับ Postgres 17 ควรส่งกลับมาพร้อมกับคะแนน ให้ส่งตัวระบุผ่าน filters ตามที่แสดงไว้ การใช้ user_id ในระดับบนสุดยังคงใช้งานได้ และเซิร์ฟเวอร์จะบันทึก Top-level user_id in /search is deprecated. Use filters={...} instead. ทุกครั้งที่คุณใช้งาน

ล้างข้อมูลหลังการทดสอบเสร็จสิ้นเพื่อไม่ให้ข้อมูลทดสอบปนเปื้อนกับการค้นหาจริง:

curl -sS -X DELETE "http://127.0.0.1:8888/memories?user_id=smoke" \
  -H "X-API-Key: $MEM0_KEY"

หากผลการค้นหาได้จำนวนแถวน้อยกว่าที่คาดไว้ ให้ตรวจสอบค่าเริ่มต้นก่อนที่จะสรุปว่าระบบดึงข้อมูลผิดพลาด ในรุ่นปัจจุบัน top_k มีค่าเริ่มต้นเป็น 20 (ลดลงจาก 100) และ threshold มีค่าเริ่มต้นเป็น 0.1 แทนที่จะไม่มีการกำหนดค่า ดังนั้นผลลัพธ์ที่ตรงกันน้อยจะถูกกรองออกโดยอัตโนมัติ เมื่อทดสอบผ่าน curl จนใช้งานได้แล้ว คุณสามารถเชื่อมต่อ endpoint เดียวกันนี้เข้ากับ agent ได้ ไม่ว่าจะเชื่อมต่อโดยตรงหรือผ่าน MCP server ที่รันอยู่บน VPS เดียวกัน

การรัน mem0 โดยไม่ใช้ OpenAI key เลย

เริ่มต้นที่ตัวขัดขวางการทำงาน เพราะคุณจะพบปัญหานี้ภายใน 5 นาทีแรก อิมเมจของเซิร์ฟเวอร์มาพร้อมกับชุดไลบรารีของผู้ให้บริการที่กำหนดไว้ตายตัว และ /configure จะปฏิเสธทุกอย่างที่อยู่นอกเหนือจากนั้น:

LLM provider 'ollama' is not bundled in this image. Bundled providers: openai, anthropic, gemini. To use another provider, install its Python package, rebuild the container, and extend BUNDLED_LLM_PROVIDERS in server/main.py.

คุณไม่จำเป็นต้องคอมไพล์อะไรใหม่ Ollama ให้บริการ API ที่เข้ากันได้กับ OpenAI ที่ /v1 ซึ่งครอบคลุม /v1/chat/completions และ /v1/embeddings และผู้ให้บริการ openai ของ mem0 ก็ยอมรับ openai_base_url ให้ชี้คีย์นั้นไปที่ Ollama แล้วการตรวจสอบที่รวมมาจะผ่าน เพราะผู้ให้บริการนั้นคือ openai จริงๆ เพียงแค่เปลี่ยนที่อยู่เท่านั้น

เพิ่ม Ollama เข้าไปในโปรเจกต์ Compose เดียวกัน:

  ollama:
    image: ollama/ollama
    restart: unless-stopped
    networks: [mem0_network]
    ports:
      - "127.0.0.1:11434:11434"
    volumes:
      - ollama_models:/root/.ollama

เพิ่ม ollama_models: ไว้ภายใต้คีย์ระดับบนสุด volumes: จากนั้นดึงโมเดลแชทหนึ่งตัวและโมเดล embedding หนึ่งตัว:

docker compose up -d ollama
docker compose exec ollama ollama pull llama3.1:8b
docker compose exec ollama ollama pull nomic-embed-text

หาก Ollama ทำงานอยู่บนโฮสต์ในฐานะ systemd unit อยู่แล้ว เช่นเดียวกับใน การรัน Ollama โดยตรงบน VPS อย่าชี้คอนเทนเนอร์ไปที่ 127.0.0.1:11434 ภายในคอนเทนเนอร์ mem0 นั้น 127.0.0.1 คือตัวคอนเทนเนอร์ mem0 เอง ให้บริการ mem0 ใช้ extra_hosts: ["host.docker.internal:host-gateway"] ตั้งค่า Environment="OLLAMA_HOST=0.0.0.0:11434" ใน systemd drop-in เพื่อให้ Ollama ฟังบนที่อยู่ที่ bridge network เข้าถึงได้ และปิดพอร์ต 11434 ที่ไฟร์วอลล์ไว้

ถามโมเดลถึงมิติของ embedding ก่อนที่คุณจะตั้งค่าใดๆ

ขั้นตอนนี้เป็นตัวตัดสินว่าการดึงข้อมูลจะทำงานได้หรือไม่

ที่เก็บข้อมูล pgvector ของ mem0 สร้างตารางด้วยความกว้างของเวกเตอร์ที่กำหนดไว้ตายตัวคือ vector vector(1536) เพราะ embedding_model_dims ตั้งค่าเริ่มต้นไว้ที่ 1536 ซึ่งเป็นความกว้างของ text-embedding-3-small จาก OpenAI ส่วน nomic-embed-text ส่งคืนค่า 768 ค่า ไม่มีส่วนใดใน mem0 ที่เปรียบเทียบตัวเลขสองตัวนี้ ดังนั้นความไม่ตรงกันจึงปรากฏขึ้นจาก Postgres ในการแทรกข้อมูลครั้งแรก:

expected 1536 dimensions, not 768

อย่าเชื่อตัวเลขในย่อหน้านี้ ให้ถามโมเดลแทน:

curl -sS http://127.0.0.1:11434/v1/embeddings \
  -H "Content-Type: application/json" \
  -d '{"model":"nomic-embed-text","input":"dimension check"}' \
  | python3 -c "import json,sys; print(len(json.load(sys.stdin)['data'][0]['embedding']))"

คำสั่งนั้นจะแสดงความกว้างที่คอลเลกชันของคุณต้องใช้ เขียนการตั้งค่าลงในไฟล์ เพราะการวางรหัสผ่าน Postgres ผ่าน shell quoting เป็นสาเหตุให้เกิดการพิมพ์ผิดในการใช้งานจริง

{
  "vector_store": {
    "provider": "pgvector",
    "config": {
      "host": "postgres",
      "port": 5432,
      "dbname": "postgres",
      "user": "postgres",
      "password": "<POSTGRES_PASSWORD from .env>",
      "collection_name": "memories_local_768",
      "embedding_model_dims": 768
    }
  },
  "llm": {
    "provider": "openai",
    "config": {
      "model": "llama3.1:8b",
      "api_key": "ollama",
      "openai_base_url": "http://ollama:11434/v1",
      "temperature": 0.2
    }
  },
  "embedder": {
    "provider": "openai",
    "config": {
      "model": "nomic-embed-text",
      "api_key": "ollama",
      "openai_base_url": "http://ollama:11434/v1"
    }
  }
}
curl -sS -X POST http://127.0.0.1:8888/configure \
  -H "Content-Type: application/json" \
  -H "X-API-Key: $MEM0_KEY" \
  -d @config.json

curl -sS http://127.0.0.1:8888/configure -H "X-API-Key: $MEM0_KEY"

การเรียกครั้งที่สองจะอ่านการตั้งค่ากลับมา ซึ่งเป็นการตรวจสอบว่าการเขียนข้อมูลสำเร็จ จากนั้นให้ทำซ้ำการทดสอบเบื้องต้นด้านบน

รายละเอียดสี่ประการใน JSON นั้นไม่ชัดเจนนัก และแต่ละจุดจะทำให้ระบบพังหากคุณตั้งค่าผิด

api_key คือสตริง ollama และ Ollama จะเพิกเฉยต่อค่าของมัน มันไม่สามารถเป็นค่าว่างได้ เพราะไลบรารีไคลเอนต์ OpenAI จะแจ้งเตือนก่อนที่คำขอใดๆ จะออกจากกระบวนการหากไม่มีการตั้งค่าคีย์ สตริงใดๆ ที่ไม่ว่างเปล่าสามารถใช้ได้

embedding_model_dims จะใช้กับ vector store และไม่มี embedding_dims บนตัว embedder โดยเจตนา mem0 จะส่งพารามิเตอร์ dimensions ของ OpenAI เฉพาะเมื่อคุณตั้งค่า embedding_dims เท่านั้น และแบ็กเอนด์ที่ไม่รองรับ Matryoshka truncation จะปฏิเสธพารามิเตอร์นั้นทันที ให้ตั้งค่าความกว้างในจุดที่สร้างตาราง และปล่อยตัว embedder ไว้ตามเดิม

collection_name เป็นค่าใหม่ mem0 สร้างตารางด้วย CREATE TABLE IF NOT EXISTS ดังนั้นการชี้ความกว้างที่ต่างออกไปไปยังคอลเลกชันที่มีอยู่เดิมจะไม่มีผลใดๆ คอลัมน์ vector(1536) เดิมจะยังคงอยู่ และการแทรกข้อมูลทุกครั้งจะล้มเหลว การเปลี่ยนความกว้างจำเป็นต้องใช้ชื่อคอลเลกชันใหม่ หรือคุณต้องลบตารางเก่าออกด้วยตนเอง

โฮสต์ใน openai_base_url คือชื่อบริการ Compose ollama ไม่ใช่ localhost คอนเทนเนอร์จะระบุตัวตนกันและกันด้วยชื่อบริการบนเครือข่ายที่ใช้ร่วมกัน

สิ่งที่คุณต้องแลกกับการใช้งานแบบ local ทั้งหมด

จงซื่อสัตย์กับตัวเองเรื่องคุณภาพ คะแนน benchmark ที่เผยแพร่ของ mem0 ถูกวัดด้วยโมเดลระดับแนวหน้าในการสกัดข้อมูล ดังนั้นให้ถือว่าคะแนนเหล่านั้นเป็นเพดานสูงสุด ไม่ใช่การคาดการณ์สำหรับโมเดลขนาด 8B บน VPS ของคุณ โมเดลขนาดเล็กจะเขียนข้อเท็จจริงที่คลุมเครือกว่า และบางครั้งส่งคืนข้อความเป็นร้อยแก้วในจุดที่ต้องการ JSON ซึ่งจะปรากฏเป็นการเรียก add ที่ส่งคืนรายการ results ว่างเปล่าโดยไม่มีข้อผิดพลาด

ความเร็วคือต้นทุนอีกประการหนึ่ง การสกัดข้อมูลด้วย CPU เพียงอย่างเดียวต้องใช้เวลาหลายวินาทีต่อการเรียก add หนึ่งครั้ง และทุกข้อความที่คุณจัดเก็บจะต้องแลกด้วยเวลานี้ โมเดลที่ทำงานนอกเหนือจาก JSON ที่ถูกร้องขอจะทำให้ปัญหานี้แย่ลง ดังนั้น การจำกัดการตอบกลับด้วย num_predict จึงเป็นการกำหนดเพดานว่าการเรียก add แต่ละครั้งจะใช้เวลานานเท่าใด หาก latency นั้นมีความสำคัญ VPS ที่ติดตั้ง GPU คือวิธีแก้ปัญหาที่ตรงจุด การเพิ่ม CPU cores ให้กับโมเดล 8B ช่วยได้น้อยกว่าที่หลายคนคาดไว้ การเปลี่ยนโมเดลเป็นวิธีที่ประหยัดกว่าการเปลี่ยนเครื่อง และ Nemotron 3.5 Lightning บน VPS จะให้แท็กที่คุณต้องดึง, RAM ที่ต้องการ และคำตอบว่าการใช้ CPU เพียงอย่างเดียวจะเร็วพอสำหรับการใช้งานหรือไม่

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

การสำรองข้อมูล: มีฐานข้อมูลสองชุด ไม่ใช่ชุดเดียว

ข้อผิดพลาดในการสำรองข้อมูล mem0 ที่พบบ่อยที่สุดคือการ dump ฐานข้อมูลเพียงรายการเดียว init-db.sh สร้างฐานข้อมูล postgres เพิ่มขึ้นมาควบคู่กับฐานข้อมูลเริ่มต้น mem0_app และแต่ละฐานข้อมูลเก็บข้อมูลคนละประเภท ฐานข้อมูล postgres เก็บ collection ของ pgvector ซึ่งก็คือ memories ส่วน mem0_app เก็บ users, sessions, API keys และ request logs แอป self-hosted แต่ละตัวแบ่งการจัดเก็บ state แตกต่างกัน จึงเป็นเหตุผลที่ photo server 2 ตัวที่ทำงานเดียวกัน ยังต้องใช้คำสั่งสำรองข้อมูลต่างกัน ดังนั้นควรอ่านก่อนว่าแอปของคุณจัดเก็บข้อมูลอะไรไว้ ก่อนจะเชื่อถือไฟล์ dump เมื่อพิจารณาจากอีกด้านหนึ่งของช่วงนี้ จะมีกรณีอย่าง ไลบรารี Jellyfin ที่สร้างใหม่ให้เหมือนร้านเช่าวิดีโอในยุค 90s ซึ่งอ่าน catalogue ทั้งหมดจากบริการอื่น จึงแทบต้องสำรองเพียง configuration ของตัวเอง ขณะที่ mem0 ต้องสำรองทั้ง 2 ฐานข้อมูล มิฉะนั้นการ restore จะใช้งานไม่ได้

หากกู้คืนเฉพาะ postgres ข้อมูลหน่วยความจำจะกลับมา แต่บัญชีผู้ใช้และ API keys ทั้งหมดจะหายไป ทำให้ไม่มีสิ่งใดสามารถยืนยันตัวตนเพื่อเข้าถึงข้อมูลเหล่านั้นได้ คุณต้องดัมพ์ข้อมูลทั้งสองชุดรวมถึงบทบาท (roles) ด้วยคำสั่งเดียวดังนี้:

docker compose exec -T postgres pg_dumpall -U postgres --clean \
  | gzip > "mem0-$(date +%F).sql.gz"

โวลุ่มประวัติการทำงาน (history volume) แยกส่วนจาก Postgres และจำเป็นต้องมีการคัดลอกแยกต่างหาก:

docker run --rm -v mem0_mem0_history:/data -v "$PWD:/backup" \
  alpine tar czf /backup/mem0-history.tgz -C /data .

Docker จะใส่ชื่อโปรเจกต์นำหน้าชื่อโวลุ่ม ดังนั้นโปรดตรวจสอบชื่อของคุณด้วย docker volume ls ก่อนที่จะสรุปว่าเป็น mem0_mem0_history

ให้กู้คืนข้อมูลลงในคอนเทนเนอร์เปล่า (scratch container) และตรวจสอบจำนวนแถวข้อมูลก่อนที่จะเชื่อถือข้อมูลเหล่านั้น:

gunzip -c mem0-2026-08-03.sql.gz \
  | docker compose exec -T postgres psql -U postgres -d postgres

การสำรองข้อมูลที่คุณไม่เคยทดสอบกู้คืนถือเป็นเพียงการคาดเดาเท่านั้น เมื่อได้ไฟล์ดัมพ์ที่ถูกต้องแล้ว ให้ส่งข้อมูลเหล่านั้นออกจากเซิร์ฟเวอร์ด้วย การทำ restic snapshots ไปยังพื้นที่จัดเก็บภายนอก เพราะการสำรองข้อมูลที่เก็บไว้บนเซิร์ฟเวอร์เครื่องเดียวกับที่ปกป้องอยู่นั้น ไม่สามารถป้องกันความเสียหายใดๆ ได้เลย

รูปแบบความล้มเหลวและข้อความที่คุณจะพบ

{"detail":"Authentication required. Provide a Bearer token or X-API-Key header."} หมายความว่าส่วนหัว (header) หายไปหรือสะกดผิด ชื่อที่ถูกต้องคือ X-API-Key และ curl จะส่งชื่อส่วนหัวตามตัวอักษรที่ระบุ

{"detail":"At least one identifier (user_id, agent_id, run_id) is required."} เมื่อมีการเพิ่มข้อมูล หมายความว่าคำขอไม่มีข้อมูลดังกล่าว หน่วยความจำ (memory) จำเป็นต้องถูกกำหนดขอบเขตไว้กับบางสิ่ง เนื่องจากตัวกรองการค้นหาจะทำงานกับฟิลด์เหล่านั้นโดยเฉพาะ

LLM provider 'ollama' is not bundled in this image พร้อมกับ HTTP 400 หมายความว่าคุณส่ง "provider": "ollama" ไป ให้ใช้ "provider": "openai" โดยชี้ openai_base_url ไปที่ Ollama

expected 1536 dimensions, not 768 จาก Postgres หมายความว่าคอลเลกชันถูกสร้างขึ้นด้วยความกว้างขนาดหนึ่ง แต่ตัว embedder ส่งค่ากลับมาอีกขนาดหนึ่ง ให้ตั้งค่า embedding_model_dims บน vector store และใช้ collection_name ใหม่

การค้นหาคืนค่าแถวที่ไม่สมเหตุสมผล หลังจากเปลี่ยนโมเดล โดยไม่มีข้อผิดพลาดใดๆ ปรากฏขึ้น เนื่องจากความกว้างยังคงตรงกัน ฐานข้อมูลจึงทำงานได้ปกติ แต่โมเดลสองตัววางประโยคเดียวกันไว้ในตำแหน่งที่ต่างกัน ให้เริ่มคอลเลกชันใหม่และเพิ่มข้อมูลเข้าไปอีกครั้ง

Connection refused ใน log ของ mem0 ขณะเชื่อมต่อกับ Ollama มักหมายถึง 127.0.0.1 ใน openai_base_url ภายในคอนเทนเนอร์ ที่อยู่ดังกล่าวจะหมายถึงตัวคอนเทนเนอร์เอง ให้ใช้ชื่อบริการ (service name) หรือ host gateway ในกรณีที่ Ollama ทำงานอยู่บนโฮสต์

504 Gateway Time-out จาก nginx เมื่อมีการเพิ่มข้อมูล หมายความว่าโมเดลใช้เวลาประมวลผลนานกว่า proxy_read_timeout ให้เพิ่มค่าดังกล่าว และตรวจสอบว่าหน่วยความจำถูกเขียนลงไปแล้วหรือไม่ก่อนที่จะลองส่งคำขอซ้ำ

exit code 137 ระหว่าง docker compose up --build หมายความว่ากระบวนการถูกสั่งหยุดด้วย SIGKILL เนื่องจากสัญญาณร้ายแรง (fatal signal) N จะถูกรายงานเป็น 128 + N และ SIGKILL คือสัญญาณที่ 9 ในขณะที่ dashboard กำลัง build บน VPS ขนาดเล็ก ผู้ที่ส่งสัญญาณนี้มักจะเป็น out-of-memory killer ดังนั้นให้รันการตรวจสอบ dmesg จากส่วนแรกก่อนดำเนินการใดๆ หากพบกระบวนการที่ถูกสั่งหยุด ให้เพิ่ม swap หรือ build image บนเครื่องที่มีขนาดใหญ่กว่าแล้ว push ไปยัง registry

error: port 3000 is already in use มาจากเป้าหมาย make up ของ repo ซึ่งปฏิเสธที่จะเริ่มทำงานเมื่อพอร์ต 3000 หรือ 8888 ถูกใช้งานอยู่ ให้ค้นหาเจ้าของพอร์ตด้วย lsof -iTCP:3000 -sTCP:LISTEN

FAQ

ฉันยังจำเป็นต้องใช้ Neo4j เพื่อรัน mem0 พร้อมกับ graph memory หรือไม่?

ไม่จำเป็น อัลกอริทึมหน่วยความจำแบบใหม่ที่ปล่อยออกมาในเดือนเมษายน 2026 ได้นำ configuration keys graph_store และ enable_graph ออกจาก open source SDK แล้ว ปัจจุบันการสกัดเอนทิตี (entity extraction) จะทำงานระหว่างการเพิ่มข้อมูลตามปกติและเขียนลงใน pgvector collection ชุดที่สองที่ชื่อว่า <collection_name>_entities ดังนั้นจึงไม่มีการใช้ฐานข้อมูลกราฟภายนอก ไม่ต้องมี container เพิ่มเติม และไม่มีขั้นตอนการตั้งค่าพิเศษ สิ่งที่เปลี่ยนไปคือฟิลด์ relations ในผลลัพธ์การค้นหาจะไม่มีอยู่อีกต่อไป ปัจจุบันเอนทิตีจะทำหน้าที่เพิ่มอันดับ (ranking) ของหน่วยความจำแทนการให้เส้นเชื่อม (edges) สำหรับการไล่ลำดับ ดังนั้นแอปพลิเคชันที่เคยไล่ลำดับความสัมพันธ์เหล่านั้นจำเป็นต้องมี graph store ของตนเองแยกต่างหากจาก mem0

VPS ขนาดเล็กที่สุดที่สามารถรัน mem0 server แบบ self-hosted ได้คือเท่าใด?

ในกรณีที่โฮสต์ language model ไว้ที่อื่น RAM ขนาด 2 GB และพื้นที่ดิสก์ว่างประมาณ 4 GB ก็เพียงพอสำหรับ API container, Postgres และ dashboard ช่วงเวลาที่ตึงตัวที่สุดคือการ build ครั้งแรก เนื่องจากการคอมไพล์ Next.js dashboard ใช้หน่วยความจำมากกว่าการรันปกติ และเครื่องขนาด 1 GB จะถูกสั่ง kill กระบวนการ build ด้วยข้อผิดพลาด exit code 137 หากรัน Ollama บนเซิร์ฟเวอร์เดียวกัน ให้เผื่อขนาดสำหรับโมเดลด้วย โดยโมเดลขนาด 8B ที่มีการทำ 4-bit quantisation จะใช้ RAM ประมาณ 6 GB ดังนั้นควรวางแผนใช้ RAM ขนาด 8 GB

ฉันสามารถรัน mem0 โดยไม่ใช้ OpenAI API key ได้หรือไม่?

ได้ ผ่านทาง endpoint ที่รองรับ OpenAI ของ Ollama การตั้งค่า "provider": "ollama" จะล้มเหลวเนื่องจาก server image รวมมาเฉพาะไลบรารี openai, anthropic และ gemini เท่านั้นและจะส่งค่า HTTP 400 กลับมา ให้คงค่า "provider": "openai" ไว้และตั้งค่า "openai_base_url": "http://ollama:11434/v1" ด้วยค่า api_key ใดๆ ที่ไม่ว่างเปล่า ทั้งสำหรับ llm และ embedder โดย Ollama จะเพิกเฉยต่อคีย์ดังกล่าว และการตรวจสอบ provider ที่รวมมาจะผ่านเนื่องจาก provider คือ openai จริงๆ

ทำไม mem0 ถึงไม่ส่งผลลัพธ์กลับมาหลังจากที่ฉันเปลี่ยนไปใช้ local embedding model?

เนื่องจากตาราง pgvector ถูกสร้างขึ้นด้วยความกว้างที่กำหนดไว้ตายตัว ค่า embedding_model_dims มีค่าเริ่มต้นเป็น 1536 ในขณะที่ nomic-embed-text ส่งค่ากลับมา 768 ทำให้ Postgres ปฏิเสธการ insert ด้วยข้อผิดพลาด expected 1536 dimensions, not 768 โดย mem0 จะสร้างตารางด้วย CREATE TABLE IF NOT EXISTS ดังนั้นการเปลี่ยนตัวเลขเพียงอย่างเดียวจะไม่มีผลต่อ collection ที่มีอยู่เดิม ให้ตั้งค่า embedding_model_dims ให้ตรงกับความกว้างจริงของโมเดลของคุณ ยืนยันความกว้างนั้นโดยการเรียก /v1/embeddings และนับจำนวนค่าที่ส่งกลับมา พร้อมกับกำหนด collection_name ใหม่ให้กับ vector store ในเวลาเดียวกัน