SSD Nodes Learn 🎉 VPS از $5.50/ماه
راهنماها Matt Connorتوسط Matt Connor · به‌روزرسانی شده 2026-08-13

آموزش میزبانی mem0 روی VPS و مدیریت منابع RAM

برای اجرای mem0 به حداقل 1 گیگابایت رم نیاز دارید. این راهنما شامل فایل Docker Compose برای اتصال به localhost، تنظیم TLS و اجرای مدل روی Ollama با حداقل 8 گیگابایت رم است.

هزینه واقعی میزبانی mem0 روی یک VPS از نظر RAM

میزبانی mem0 به معنای اجرای سه کانتینر است: سرور FastAPI برای حافظه، Postgres با افزونه pgvector و یک داشبورد Next.js. سرویس mem0 یک لایه حافظه برای ایجنت‌هاست. شما یک مکالمه را به آن ارسال می‌کنید، یک مدل زبانی حقایق پایدار را از آن مکالمه استخراج می‌کند و این حقایق به صورت بردار ذخیره می‌شوند تا در پرس‌وجوهای بعدی، موارد مرتبط بازیابی شوند.

تقریباً 1 گیگابایت حافظه Resident برای این سه کانتینر و 3 تا 4 گیگابایت فضا روی دیسک پس از ساخته‌شدن ایمیج‌ها در نظر بگیرید. یک VPS با 2 گیگابایت رم این مجموعه را به‌راحتی اجرا می‌کند، به شرطی که مدل زبانی در جای دیگری میزبانی شود. زمانی که مدل از طریق Ollama روی همان سرور اجرا شود، مدل بر تمام منابع دیگر غلبه می‌کند: یک مدل 8B که با دقت 4 بیت کوانتایز شده، به تنهایی حدود 6 گیگابایت رم نیاز دارد، بنابراین برای یک پیاده‌سازی کاملاً محلی، حداقل 8 گیگابایت رم لازم است.

به این اعداد در پست‌های وبلاگی، از جمله همین متن، اکتفا نکنید. پشته‌ای که واقعاً ساخته‌اید را اندازه‌گیری کنید.

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

docker stats میزان حافظه Resident هر کانتینر را چاپ می‌کند. docker system df -v میزان فضای دیسک اشغال‌شده توسط هر ایمیج و هر Volume را نمایش می‌دهد.

وضعیت پایدار (Steady state) همان اوج مصرف نیست. docker compose up -d --build داشبورد Next.js را کامپایل می‌کند و آن فرآیند build در Node، حریص‌ترین لحظه در کل نصب است. روی یک VPS با 1 گیگابایت رم، قابلیت OOM Killer در هسته سیستم‌عامل آن را متوقف می‌کند و فرآیند build با exit code 137 پایان می‌یابد. پیش از آنکه به دنبال باگ در Docker بگردید، علت را تأیید کنید:

dmesg -T | grep -i "killed process"

اگر احساس می‌کنید این سرور برای نیاز شما بیش از حد سنگین است، گزینه‌های کوچک‌تری نیز وجود دارند. یک حافظه محلی برای ایجنت بدون نیاز به سرور و حافظه‌ای که درون خود Claude Code زندگی می‌کند، هر دو پایگاه داده را حذف می‌کنند. هر زمان که چندین ایجنت یا چندین ماشین نیاز داشتند از حافظه‌های یکسانی استفاده کنند، به اینجا بازگردید.

آیا برای حافظه گرافی mem0 به Neo4j نیاز دارم؟

خیر. اگر راهنمایی به شما می‌گوید که یک کانتینر Neo4j اضافه کنید، آن راهنما قدیمی‌تر از کد فعلی است.

حافظه گرافی در mem0 قبلاً به معنای استفاده از یک پایگاه‌داده گرافی خارجی بود که تحت کلید graph_store با مقدار true برای enable_graph پیکربندی می‌شد. الگوریتم حافظه جدید که در آوریل 2026 عرضه شد، هر دو کلید را از SDK متن‌باز حذف کرده است. استخراج موجودیت‌ها (Entity extraction) اکنون در مسیر عادی add اجرا می‌شود و موجودیت‌ها در یک collection از نوع pgvector نوشته می‌شوند که نام آن از روی collection اصلی شما گرفته شده و پسوند _entities به آن اضافه می‌شود. هیچ عملیات مهاجرتی برای اجرا وجود ندارد. قابلیت پیونددهی موجودیت‌ها (Entity linking) به‌صورت داخلی از همان فراخوانی add بعدی شروع به کار می‌کند.

حذف کردن graph store باعث صرفه‌جویی در یک کانتینر JVM، حافظه heap آن و چندین صد مگابایت حجم image می‌شود. روی یک VPS با 2 GB رم، این تفاوت بین اجرای روان و درگیر شدن با swap است.

در اینجا آنچه را که از دست می‌دهید، به‌صراحت بیان می‌کنیم. نتایج جستجو قبلاً دارای فیلد relations بودند که یال‌های بین موجودیت‌ها را فهرست می‌کرد. آن فیلد حذف شده است. تطبیق موجودیت‌ها اکنون جایگاه یک حافظه را در امتیاز ترکیبی (combined score) ارتقا می‌دهد و دیگر ساختاری وجود ندارد که بتوانید در آن پیمایش کنید. اگر برنامه شما آن روابط را پیمایش می‌کرد، mem0 دیگر آن‌ها را نگهداری نمی‌کند و شما باید پایگاه‌داده گرافی خود را خارج از mem0 داشته باشید که توسط کد خودتان تغذیه می‌شود.

فایل compose موجود در مخزن، یک فایل توسعه (development) است

server/docker-compose.yaml متغیر name: mem0-dev را تعریف می‌کند و منظور آن دقیقاً همین است. پیش از اجرای آن، فایل را مطالعه کنید، زیرا پنج مورد در آن برای یک سرور اشتباه است.

  • این فایل از server/dev.Dockerfile بیلد می‌گیرد و با استفاده از .:/app، دایرکتوری checkout شما را روی ایمیج mount می‌کند؛ بنابراین کانتینر به جای آنچه بیلد کرده‌اید، هر چیزی که در آن دایرکتوری قرار دارد را اجرا می‌کند.
  • دستور آن 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 می‌کنید.
  • پورت‌های منتشر شده (published) عبارتند از "8888:8000"، "8432:5432" و "3000:3000". پورت منتشر شده‌ای که آدرسی پیش از آن ذکر نشده باشد، به 0.0.0.0 متصل می‌شود؛ بنابراین به محض شروع stack، دیتابیس Postgres روی پورت 8432 به اینترنت عمومی پاسخ می‌دهد.

مورد آخر نیازمند هشدار جداگانه‌ای است. Docker با نوشتن قوانین اختصاصی خود پیش از زنجیره‌ای که 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 شروع می‌شود، بنابراین هسته سیستم‌عامل فقط اتصالات را از خودِ همان سرور می‌پذیرد. تمام ترافیک ورودی از خارج، از طریق reverse proxy عبور می‌کند که تنها بخشی است که گواهی TLS را در اختیار دارد.

سرویس Postgres اصلاً بلاک ports ندارد. کانتینر mem0 از طریق mem0_network و با استفاده از نام سرویس به آن متصل می‌شود، بنابراین انتشار پورت 8432 هیچ مزیتی ندارد و فقط یک پورت باز اضافه ایجاد می‌کند. هر زمان که به shell نیاز داشتید از docker compose exec postgres psql -U postgres استفاده کنید.

تاریخچه از bind mount در ./history به یک volume نام‌گذاری‌شده منتقل می‌شود. یک bind mount داده‌ها را به یک مسیر و یک uid خاص روی میزبان گره می‌زند، در حالی که volume نام‌گذاری‌شده شیئی است که Docker می‌تواند از آن snapshot بگیرد و جابه‌جا کند. تفاوت volumeهای نام‌گذاری‌شده و bind mountها توضیح می‌دهد که هر کدام در چه زمانی مناسب هستند.

دستور، --reload را حذف کرده و alembic upgrade head را حفظ می‌کند. این مرحله مهاجرت را نگه دارید. بدون آن، برنامه با یک دیتابیس بدون جدول بالا می‌آید و هر درخواست در اولین کوئری با خطا مواجه می‌شود.

NEXT_PUBLIC_API_URL همان URLای است که مرورگر شما فراخوانی می‌کند، بنابراین باید آدرس عمومی HTTPS باشد و نه http://mem0:8000. فریم‌ورک Next.js تمام مقادیر NEXT_PUBLIC_ را در زمان build درون کدها قرار می‌دهد (inline می‌کند)، بنابراین تغییر آن نیازمند docker compose up -d --build mem0-dashboard است. یک restart ساده باعث می‌شود مقدار قدیمی همچنان در فایل‌های JavaScript باقی بماند و داشبورد به 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 را به حال خود رها کنید. نام این پرچم به‌خوبی عملکرد آن را توصیف می‌کند: با فعال بودن آن، سرور تمام حافظه‌ای که در اختیار دارد را به هر کسی که به پورت دسترسی داشته باشد، ارائه می‌دهد. اگر نمی‌خواهید رویداد onboarding به سمت بالا (upstream) ارسال شود، MEM0_TELEMETRY=false را تنظیم کنید.

مقدار ADMIN_API_KEY با هدر X-API-Key و با استفاده از secrets.compare_digest مقایسه می‌شود و در صورت تطابق، از هرگونه جستجوی پایگاه داده صرف‌نظر می‌شود. این یک اعتبارنامه ریشه (root credential) برای کل API است. با آن به همین شکل رفتار کنید: نه در تاریخچه shell، نه در git و نه با کپی کردن آن در یک prompt. مطالب نحوه ترکیب فایل‌های env و محل نشت اسرار از آن‌ها و دور نگه داشتن کلیدهای API از context یک agent هر دو مستقیماً در اینجا صدق می‌کنند، زیرا فراخوانندگان این سرور، agentها هستند.

مقادیر بارگذاری‌شده از env_file در محیط container قرار می‌گیرند و docker inspect آن‌ها را به‌طور کامل چاپ می‌کند. هر کسی در گروه docker می‌تواند آن‌ها را بخواند و هر کسی در گروه docker عملاً در میزبان (host) دسترسی ریشه (root) دارد.

قرار دادن TLS در مقابل API به‌جای باز کردن پورت 8888

سرویس API روی 127.0.0.1:8888 و داشبورد روی 127.0.0.1:3000 پاسخ می‌دهند. Nginx عملیات TLS (امنیت لایه انتقال) را روی پورت 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 مواجه می‌شود در حالی که مدل همچنان در حال پردازش است و حافظه همچنان در حال نوشته شدن است. در نهایت با حافظه‌ای مواجه می‌شوید که به شما اعلام شده بود با شکست مواجه شده است.

باقی پورت‌ها را با سیاست پیش‌فرض deny در ufw ببندید و فقط پورت‌های 22 و 443 را باز بگذارید. گواهی را با استفاده از certbot روی Ubuntu 24.04 پشت Nginx صادر کنید. اگر سرور در حال حاضر با Traefik برای مسیریابی چندین برنامه Compose از سایر برنامه‌ها میزبانی می‌کند، mem0 را به همان مسیریاب اضافه کنید و از نصب پروکسی دوم خودداری نمایید.

تست سلامت: افزودن یک حافظه و خواندن مجدد آن

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 باید همراه با یک امتیاز (score) بازگردانده شود. شناسه را مطابق نمایش داده شده، درون 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"

اگر جستجو ردیف‌های کمتری نسبت به انتظار شما بازگرداند، پیش از آنکه بازیابی (retrieval) را مقصر بدانید، مقادیر پیش‌فرض را بررسی کنید. در نسخه فعلی، top_k به‌طور پیش‌فرض روی 20 تنظیم شده است (که از 100 کاهش یافته) و threshold به‌جای مقدار none، به‌طور پیش‌فرض روی 0.1 تنظیم شده است؛ بنابراین مطابقت‌های ضعیف اکنون برای شما فیلتر می‌شوند. هنگامی که این عملیات با curl به‌درستی کار کرد، همین endpointها همان‌هایی هستند که باید به یک agent متصل کنید، چه به‌صورت مستقیم و چه از طریق یک سرور MCP که روی همان VPS اجرا می‌شود.

اجرای mem0 بدون هیچ کلید OpenAI

با مانع اصلی شروع کنید، چرا که در همان پنج دقیقه اول با آن مواجه خواهید شد. ایمیج سرور مجموعه‌ای ثابت از کتابخانه‌های ارائه‌دهنده را به همراه دارد و /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 بگیرید تا بررسی بسته‌بندی‌شده (bundled check) با موفقیت انجام شود، زیرا ارائه‌دهنده در واقع همان 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 را pull کنید:

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

اگر Ollama قبلاً روی میزبان به عنوان یک unit در systemd اجرا می‌شود، همان‌طور که در اجرای مستقیم 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" را در یک فایل drop-in برای systemd تنظیم کنید تا Ollama روی آدرسی گوش دهد که bridge به آن دسترسی دارد، و پورت 11434 را در فایروال بسته نگه دارید.

پیش از هرگونه پیکربندی، ابعاد embedding مدل را از آن بپرسید

این یک مرحله تعیین می‌کند که آیا بازیابی (retrieval) اصلاً کار می‌کند یا خیر.

فروشگاه pgvector در mem0 جدول خود را با عرض بردار ثابت، vector vector(1536)، ایجاد می‌کند، زیرا embedding_model_dims به‌طور پیش‌فرض روی 1536 تنظیم شده است که عرض مدل text-embedding-3-small در OpenAI است. مدل nomic-embed-text تعداد 768 مقدار برمی‌گرداند. هیچ‌چیز در mem0 این دو عدد را با هم مقایسه نمی‌کند، بنابراین عدم تطابق در اولین insert از سمت 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']))"

این دستور عرضی که مجموعه (collection) شما باید استفاده کند را چاپ می‌کند. پیکربندی را در یک فایل بنویسید، زیرا وارد کردن رمز عبور Postgres از طریق shell quoting باعث ایجاد غلط‌های تایپی در محیط عملیاتی (production) می‌شود.

{
  "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 بالا را تکرار کنید.

چهار جزئیات در آن JSON بدیهی نیستند و اگر هر کدام را اشتباه انجام دهید، چیزی از کار می‌افتد.

api_key همان رشته ollama است و Ollama مقدار آن را نادیده می‌گیرد. این مقدار نمی‌تواند خالی باشد، زیرا کتابخانه کلاینت OpenAI پیش از خروج هر درخواستی از پردازش، در صورت تنظیم نبودن کلید، خطا می‌دهد. هر رشته غیرخالی کار می‌کند.

embedding_model_dims روی vector store اعمال می‌شود و عمداً هیچ embedding_dims روی embedder وجود ندارد. mem0 پارامتر dimensions مربوط به OpenAI را فقط زمانی ارسال می‌کند که embedding_dims را تنظیم کنید، و backendهایی که Matryoshka truncation را پیاده‌سازی نکرده‌اند، آن پارامتر را مستقیماً رد می‌کنند. عرض را در جایی که جدول ایجاد می‌شود تنظیم کنید و embedder را به حال خود بگذارید.

collection_name جدید است. mem0 جدول خود را با CREATE TABLE IF NOT EXISTS ایجاد می‌کند، بنابراین اشاره کردن به یک عرض متفاوت برای یک مجموعه موجود هیچ کاری انجام نمی‌دهد: ستون vector(1536) قدیمی باقی می‌ماند و هر insert با شکست مواجه می‌شود. تغییر عرض نیاز به یک نام مجموعه جدید دارد، یا باید جدول قدیمی را به‌صورت دستی حذف کنید.

میزبان در openai_base_url همان نام سرویس Compose یعنی ollama است، نه localhost. کانتینرها یکدیگر را با نام سرویس در شبکه مشترکشان پیدا می‌کنند.

هزینه مسیر کاملاً محلی چیست

در مورد کیفیت با خود صادق باشید. امتیازات بنچمارک منتشر شده mem0 با استفاده از مدل‌های پیشرو (frontier models) برای استخراج اندازه‌گیری شده‌اند، بنابراین آن‌ها را به عنوان یک سقف در نظر بگیرید، نه پیش‌بینی برای یک مدل 8B روی VPS خودتان. یک مدل کوچک، حقایق مبهم‌تری می‌نویسد و گاهی اوقات به جای JSON درخواست‌شده، متن عادی برمی‌گرداند که به صورت یک فراخوانی add که لیست results خالی بدون خطا برمی‌گرداند، ظاهر می‌شود.

سرعت هزینه دیگر است. استخراج فقط با CPU برای هر فراخوانی add ثانیه‌ها طول می‌کشد و هر پیامی که ذخیره می‌کنید این هزینه را می‌پردازد. اگر آن تأخیر (latency) اهمیت دارد، یک VPS با GPU متصل راه حل صادقانه است. اضافه کردن هسته‌های CPU بیشتر به یک مدل 8B بسیار کمتر از آنچه مردم انتظار دارند کمک می‌کند.

یک قانون فارغ از انتخاب شما پابرجاست: هرگز مدل‌های embedding را در یک مجموعه ترکیب نکنید. دو مدل متفاوت که اتفاقاً عرض یکسانی دارند، بردارهایی تولید می‌کنند که قابل مقایسه نیستند. عملیات insert با موفقیت انجام می‌شود، جستجو ردیف‌هایی را برمی‌گرداند، اما ردیف‌ها اشتباه هستند و هیچ‌جایی هم خطایی گزارش نمی‌شود.

پشتیبان‌گیری: دو پایگاه‌داده وجود دارد، نه یکی

رایج‌ترین اشتباه در پشتیبان‌گیری از mem0، دامپ گرفتن از تنها یک پایگاه‌داده است. init-db.sh در کنار پایگاه‌داده پیش‌فرض postgres، یک پایگاه‌داده mem0_app نیز ایجاد می‌کند و این دو داده‌های متفاوتی را در خود نگه می‌دارند. پایگاه‌داده postgres شامل collectionهای pgvector است که همان حافظه‌ها (memories) هستند. پایگاه‌داده mem0_app کاربران، نشست‌ها (sessions)، کلیدهای API و لاگ‌های درخواست را ذخیره می‌کند.

اگر فقط postgres را بازیابی کنید، حافظه‌ها بازمی‌گردند اما تمام حساب‌های کاربری و کلیدهای API از بین می‌روند؛ در نتیجه هیچ‌چیز نمی‌تواند برای خواندن حافظه‌ها احراز هویت شود. از هر دو پایگاه‌داده به همراه نقش‌ها (roles) در یک دستور دامپ بگیرید:

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

Volume مربوط به تاریخچه (history) از Postgres جداست و نیاز به کپی مخصوص خود دارد:

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

Docker نام volumeها را با نام پروژه پیشوندگذاری می‌کند، بنابراین پیش از فرض کردن نام mem0_mem0_history، آن را با دستور docker volume ls تأیید کنید.

پیش از آنکه به صحت داده‌ها اطمینان کنید، آن‌ها را در یک container موقت بازیابی کرده و تعداد ردیف‌ها را بررسی کنید:

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

پشتیبانی که هرگز بازیابی نشده است، صرفاً یک حدس است. هنگامی که دامپ‌ها صحیح بودند، آن‌ها را با استفاده از اسنپ‌شات‌های restic به فضای ذخیره‌سازی خارج از سایت از سرور خارج کنید؛ زیرا پشتیبانی که روی همان سرورِ تحت حفاظت باقی بماند، هیچ‌چیز را محافظت نمی‌کند.

حالت‌های شکست و رشته‌های متنی دقیق که مشاهده خواهید کرد

{"detail":"Authentication required. Provide a Bearer token or X-API-Key header."} به این معنی است که هدر وجود ندارد یا اشتباه نوشته شده است. نام آن X-API-Key است و curl نام هدرها را دقیقاً همان‌طور که هستند ارسال می‌کند.

{"detail":"At least one identifier (user_id, agent_id, run_id) is required."} در هنگام افزودن (add) به این معنی است که درخواست فاقد آن‌ها بوده است. حافظه (memory) باید به چیزی محدود (scope) شود، زیرا فیلترهای جستجو دقیقاً روی همان فیلدها عمل می‌کنند.

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 به این معنی است که مجموعه (collection) با یک عرض (width) خاص ایجاد شده، اما مدل تعبیه‌ساز (embedder) عرض دیگری را برمی‌گرداند. مقدار embedding_model_dims را در vector store تنظیم کرده و از یک collection_name جدید استفاده کنید.

جستجو ردیف‌هایی را برمی‌گرداند که بی‌معنی هستند؛ این اتفاق پس از تغییر مدل رخ می‌دهد و هیچ خطایی در هیچ‌جا ثبت نمی‌شود. عرض (width) همچنان مطابقت دارد، بنابراین پایگاه داده مشکلی ندارد، اما دو مدل مختلف، یک جمله مشابه را در موقعیت‌های متفاوتی قرار می‌دهند. یک مجموعه جدید ایجاد کرده و داده‌ها را دوباره اضافه کنید.

Connection refused در لاگ‌های mem0 هنگام برقراری ارتباط با Ollama معمولاً به معنی 127.0.0.1 در openai_base_url است. در داخل کانتینر، آن آدرس به خود کانتینر اشاره دارد. از نام سرویس استفاده کنید، یا اگر Ollama روی میزبان (host) اجرا می‌شود، از host gateway استفاده نمایید.

504 Gateway Time-out از سمت nginx در هنگام افزودن (add) به این معنی است که پردازش مدل بیش از proxy_read_timeout طول کشیده است. این مقدار را افزایش دهید و پیش از تلاش مجدد برای ارسال درخواست، بررسی کنید که آیا حافظه (memory) قبلاً نوشته شده است یا خیر.

exit code 137 در حین docker compose up --build به این معنی است که OOM killer (قاتل کمبود حافظه) در حال متوقف کردن فرآیند ساخت داشبورد است. فضای swap اضافه کنید یا image را روی دستگاهی با منابع بیشتر بسازید و سپس آن را به یک registry ارسال (push) کنید.

error: port 3000 is already in use از هدف make up در مخزن (repo) ناشی می‌شود که وقتی پورت‌های 3000 یا 8888 اشغال باشند، از شروع کار خودداری می‌کند. مالک فرآیند را با lsof -iTCP:3000 -sTCP:LISTEN پیدا کنید.

FAQ

آیا برای اجرای mem0 با حافظه گرافی (graph memory) همچنان به Neo4j نیاز دارم؟

خیر. الگوریتم حافظه جدید که در آوریل 2026 عرضه شد، کلیدهای پیکربندی graph_store و enable_graph را از SDK متن‌باز حذف کرده است. استخراج موجودیت‌ها (Entity extraction) اکنون در حین یک عملیات افزودن عادی انجام شده و در یک مجموعه pgvector دوم به نام <collection_name>_entities نوشته می‌شود؛ بنابراین دیگر نیازی به دیتابیس گرافی خارجی، کانتینر اضافی یا مرحله مهاجرت داده نیست. در عوض، فیلد relations در نتایج جستجو دیگر وجود ندارد. موجودیت‌ها اکنون به‌جای ارائه یال‌هایی برای پیمایش، رتبه حافظه را افزایش می‌دهند؛ بنابراین برنامه‌ای که از آن روابط استفاده می‌کرد، باید ذخیره‌ساز گراف اختصاصی خود را خارج از mem0 داشته باشد.

کوچک‌ترین VPS برای اجرای سرور mem0 به صورت self-hosted چیست؟

اگر مدل زبانی در جای دیگری میزبانی شود، 2 گیگابایت رم و حدود 4 گیگابایت فضای دیسک خالی برای کانتینر API، Postgres و داشبورد کافی است. مرحله حساس، اولین بیلد (build) است، زیرا کامپایل کردن داشبورد Next.js حافظه بیشتری نسبت به اجرای آن مصرف می‌کند و یک سرور 1 گیگابایتی با خطای exit code 137 متوقف می‌شود. اگر Ollama روی همان سرور اجرا می‌شود، باید حجم مدل را نیز در نظر بگیرید: یک مدل 8B با کوانتیزاسیون 4-bit به تنهایی حدود 6 گیگابایت رم نیاز دارد، بنابراین برای 8 گیگابایت برنامه‌ریزی کنید.

آیا می‌توانم mem0 را بدون کلید API شرکت OpenAI اجرا کنم؟

بله، از طریق endpoint سازگار با OpenAI در Ollama. تنظیم "provider": "ollama" با شکست مواجه می‌شود، زیرا ایمیج سرور فقط کتابخانه‌های openai، anthropic و gemini را شامل می‌شود و خطای HTTP 400 برمی‌گرداند. در عوض، "provider": "openai" را حفظ کرده و "openai_base_url": "http://ollama:11434/v1" را با هر مقدار غیرخالی در api_key، هم برای llm و هم برای embedder تنظیم کنید. Ollama این کلید را نادیده می‌گیرد و بررسی ارائه‌دهنده (provider) در بسته نرم‌افزاری نیز با موفقیت انجام می‌شود، زیرا ارائه‌دهنده در واقع همان openai است.

چرا mem0 پس از تغییر به مدل embedding محلی، هیچ نتیجه‌ای برنمی‌گرداند؟

زیرا جدول pgvector با عرض ثابت ایجاد شده است. مقدار پیش‌فرض embedding_model_dims برابر با 1536 است، در حالی که nomic-embed-text مقدار 768 را برمی‌گرداند و Postgres عملیات درج را با خطای expected 1536 dimensions, not 768 رد می‌کند. mem0 جدول را با CREATE TABLE IF NOT EXISTS ایجاد می‌کند، بنابراین تغییر دادن عدد به تنهایی تأثیری بر مجموعه موجود ندارد. مقدار embedding_model_dims را روی عرض واقعی مدل خود تنظیم کنید، آن عرض را با فراخوانی /v1/embeddings و شمارش مقادیر بازگشتی تأیید کنید و همزمان یک collection_name جدید به ذخیره‌ساز برداری اختصاص دهید.