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

วิธี self-host mem0 บน VPS พร้อมตั้งค่า Ollama และ RAM

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

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

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

ให้เตรียม RAM ไว้ประมาณ 1 GB สำหรับคอนเทนเนอร์ทั้ง 3 ตัว และพื้นที่ดิสก์ 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 หรือไม่

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

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

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

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

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

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

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

ประเด็นสุดท้ายนี้ควรได้รับคำเตือนเป็นพิเศษ Docker จะเปิดพอร์ตโดยการเขียนกฎของตัวเองนำหน้า chain ที่ ufw จัดการอยู่ ดังนั้น ufw deny 8432 จึงไม่สามารถปิดพอร์ตของคอนเทนเนอร์ที่ถูกเปิดไว้ได้ การที่ 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 เลย คอนเทนเนอร์ 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 ดังนั้นการเปลี่ยนแปลงค่านี้จึงจำเป็นต้องใช้ docker compose up -d --build mem0-dashboard การ restart แบบปกติจะยังคงใช้ค่าเดิมที่ถูกฝังอยู่ใน JavaScript ทำให้แดชบอร์ดเรียกไปยังโฮสต์ที่ผิดพลาด

ข้อมูลลับต้องอยู่ใน .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 จะอยู่ในสภาพแวดล้อมของ container และ docker inspect จะแสดงค่าเหล่านั้นออกมาทั้งหมด ผู้ที่อยู่ในกลุ่ม docker สามารถอ่านค่าเหล่านี้ได้ และทุกคนที่อยู่ในกลุ่ม docker จะมีสิทธิ์เทียบเท่า root บนโฮสต์นั้น

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

API ตอบสนองที่ 127.0.0.1:8888 และแดชบอร์ดที่ 127.0.0.1:3000 โดยให้ nginx ทำหน้าที่ยุติ 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 จะค้างไว้ในขณะที่โมเดลภาษาอ่านบทสนทนาและดึงข้อมูลสำคัญออกมา โมเดลขนาด 8B ที่รันบน CPU ในเครื่องมักใช้เวลาประมวลผลนานกว่าค่าเริ่มต้น 60 วินาทีของ nginx ส่งผลให้ผู้เรียกเห็น 504 Gateway Time-out ในขณะที่โมเดลยังคงทำงานอยู่และหน่วยความจำยังคงถูกเขียนลงฐานข้อมูล ผลลัพธ์คือคุณจะได้หน่วยความจำที่ระบบแจ้งว่าบันทึกล้มเหลว

ปิดพอร์ตที่เหลือด้วย นโยบาย default deny ของ ufw โดยเปิดไว้เพียงพอร์ต 22 และ 443 ออกใบรับรองด้วย certbot บน Ubuntu 24.04 หลัง nginx หากเซิร์ฟเวอร์ของคุณทำหน้าที่เป็นหน้าด่านให้กับแอปอื่นอยู่แล้วด้วย Traefik ที่จัดการ routing สำหรับแอป Compose หลายตัว ให้เพิ่ม 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.

คุณไม่จำเป็นต้อง build อะไรใหม่ Ollama ให้บริการ API ที่เข้ากันได้กับ OpenAI ที่ /v1 ซึ่งครอบคลุม /v1/chat/completions และ /v1/embeddings และ provider แบบ openai ของ mem0 ก็ยอมรับ openai_base_url ให้ชี้ key นั้นไปที่ Ollama แล้วการตรวจสอบที่รวมมาจะผ่าน เพราะตัว provider นั้นเป็น 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: ไว้ภายใต้ key ระดับบนสุดคือ 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 อย่าชี้ container ไปที่ 127.0.0.1:11434 ภายใน container ของ mem0 นั้น 127.0.0.1 คือตัว container ของ 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 dimension ก่อนที่คุณจะตั้งค่าใดๆ

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

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

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']))"

คำสั่งนั้นจะแสดงความกว้างที่ collection ของคุณต้องใช้ ให้เขียนการตั้งค่าลงในไฟล์ เพราะการวางรหัสผ่าน 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"

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

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

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

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

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

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

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

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

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

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

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

ข้อผิดพลาดที่พบบ่อยที่สุดในการสำรองข้อมูล mem0 คือการดัมพ์ฐานข้อมูลเพียงชุดเดียว init-db.sh จะสร้าง mem0_app ขึ้นมาควบคู่กับฐานข้อมูลเริ่มต้นอย่าง postgres ซึ่งทั้งสองชุดเก็บข้อมูลที่แตกต่างกัน โดยฐานข้อมูล postgres จะเก็บ pgvector collections ซึ่งก็คือหน่วยความจำ ส่วน mem0_app จะเก็บข้อมูลผู้ใช้, เซสชัน, API keys และบันทึกการร้องขอ (request logs)

หากกู้คืนเฉพาะ 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 หมายความว่าคอลเลกชันถูกสร้างขึ้นด้วยความกว้างขนาดหนึ่ง แต่ตัวสร้าง embedding ส่งค่ากลับมาอีกขนาดหนึ่ง ให้ตั้งค่า 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 คือการที่ out-of-memory killer สั่งหยุดการ build dashboard ให้เพิ่ม swap หรือ build image บนเครื่องที่มีทรัพยากรสูงกว่าแล้ว push ไปยัง registry

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

FAQ

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

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

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

ในกรณีที่โฮสต์ language model ไว้ที่อื่น RAM ขนาด 2 GB และพื้นที่ดิสก์ว่างประมาณ 4 GB ก็เพียงพอสำหรับ API container, Postgres และแดชบอร์ด ช่วงที่ต้องใช้ทรัพยากรมากที่สุดคือการ build ครั้งแรก เนื่องจากการคอมไพล์ Next.js แดชบอร์ดใช้หน่วยความจำมากกว่าการรันปกติ และเครื่องขนาด 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 จะเพิกเฉยต่อ key ดังกล่าว และการตรวจสอบ 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 ในเวลาเดียวกัน