SSD Nodes Learn Hosting plans →
راهنماها Matt Connorتوسط Matt Connor · به‌روزرسانی شده 2026-08-28

آموزش میزبانی mem0 روی VPS: راهنمای عملی و بهینه

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

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

میزبانی 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 را نمایش می‌دهد.

وضعیت پایدار، اوج مصرف نیست. دستور docker compose up -d --build داشبورد Next.js را کامپایل می‌کند و آن فرآیند build در Node، پرمصرف‌ترین لحظه در کل نصب است. روی یک VPS با 1 گیگابایت رم، قابلیت out-of-memory killer در هسته سیستم‌عامل آن را متوقف می‌کند و فرآیند build با خطای exit code 137 پایان می‌یابد. پیش از آنکه به دنبال باگ در Docker بگردید، علت را تأیید کنید:

dmesg -T | grep -i "killed process"

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

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

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

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

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

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

فایل compose موجود در مخزن، یک فایل compose برای محیط توسعه است

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

  • این فایل از server/dev.Dockerfile بیلد می‌گیرد و دایرکتوری جاری شما را با .:/app روی ایمیج mount می‌کند؛ بنابراین کانتینر به جای آنچه بیلد کرده‌اید، هر فایلی که در آن دایرکتوری باشد را اجرا می‌کند.
  • دستور آن rm -rf /app/packages && pip install -q --force-reinstall --no-deps mem0ai && alembic upgrade head && uvicorn main:app --reload است. این دستور در هر بار شروع، mem0ai را از PyPI دوباره نصب می‌کند؛ بنابراین نسخه‌ای که سرور شما اجرا می‌کند ممکن است در حین یک restart که تصور نمی‌کردید ارتقا باشد، تغییر کند.
  • همان مرحله pip به این معنی است که اگر در هنگام restart دسترسی به شبکه خارجی نداشته باشید، فرآیند پیش از اجرای uvicorn شکست می‌خورد. در نتیجه سرور حافظه شما به دلیل عدم دسترسی به PyPI از دسترس خارج می‌شود.
  • --reload قابلیت file watcher مربوط به uvicorn را فعال می‌کند. این قابلیت برای restart کردن فرآیند هنگام ویرایش کد است و در محیط production، بدون هیچ فایده‌ای، حافظه مصرف کرده و یک فرآیند اضافه ایجاد می‌کند. فایل Dockerfile در محیط production نیز شامل --reload در بخش CMD خود است، بنابراین در هر صورت شما دستور را override می‌کنید.
  • پورت‌های منتشر شده (published) عبارتند از "8888:8000"، "8432:5432" و "3000:3000". پورت منتشر شده‌ای که آدرس مشخصی پیش از آن تعریف نشده باشد، روی 0.0.0.0 bind می‌شود؛ بنابراین به محض شروع stack، دیتابیس Postgres روی پورت 8432 به اینترنت عمومی پاسخ می‌دهد.

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

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

تاریخچه از bind mount در ./history به یک named volume منتقل می‌شود. یک bind mount داده‌ها را به یک مسیر و یک uid خاص روی این میزبان گره می‌زند، در حالی که یک named volume یک شیء است که Docker می‌تواند از آن snapshot بگیرد و جابه‌جا کند. Named volumes against bind mounts توضیح می‌دهد که هر کدام در چه زمانی مناسب هستند.

دستور، --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 برای کل API است. با آن مانند یک اعتبارنامه root رفتار کنید: آن را در تاریخچه shell ذخیره نکنید، در git قرار ندهید و در هیچ promptای paste نکنید. تنظیم فایل‌های env و محل نشت اسرار از آن‌ها و دور نگه داشتن کلیدهای API از context عامل‌ها هر دو مستقیماً در اینجا صدق می‌کنند، زیرا فراخوانندگان این سرور، عامل‌ها (agents) هستند.

مقادیر بارگذاری‌شده از 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 server که روی همان VPS اجرا می‌شود.

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

با مانع اصلی شروع کنید، چرا که در همان 5 دقیقه اول با آن مواجه خواهید شد. ایمیج سرور مجموعه‌ای ثابت از کتابخانه‌های ارائه‌دهنده (provider) را به همراه دارد و /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 را 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 از قبل روی میزبان به عنوان یک واحد 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 باعث ایجاد غلط‌های تایپی در محیط عملیاتی می‌شود.

{
  "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 چندین ثانیه زمان می‌برد و هر پیامی که ذخیره می‌کنید، این هزینه را می‌پردازد. مدلی که بیش از حدِ JSON درخواست‌شده پرگویی می‌کند، این وضعیت را بدتر می‌کند، بنابراین محدود کردن پاسخ با num_predict سقفی برای مدت زمان اجرای هر فراخوانی add تعیین می‌کند. اگر این تأخیر (latency) اهمیت دارد، یک VPS با GPU متصل راهکار صادقانه است. افزایش هسته‌های CPU برای یک مدل 8B بسیار کمتر از حد انتظار کمک می‌کند. تغییر مدل اهرم ارزان‌تری نسبت به تغییر ماشین است و Nemotron 3.5 Lightning روی VPS تگ مورد نیاز برای pull کردن، رم مورد نیاز و اینکه آیا اجرای فقط با CPU برای استفاده روزمره کافی است یا خیر را به شما نشان می‌دهد.

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

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

رایج‌ترین اشتباه در پشتیبان‌گیری از mem0، dump گرفتن از فقط یک database است. init-db.sh، در کنار database پیش‌فرض postgres، mem0_app را ایجاد می‌کند و این دو داده‌های متفاوتی را نگه می‌دارند. database مربوط به postgres، مجموعه‌های pgvector را نگه می‌دارد که همان memoryها هستند. mem0_app نیز کاربران، sessionها، API keyها و logهای درخواست را نگه می‌دارد. هر app مبتنی بر self-hosting، state خود را به روش متفاوتی تفکیک می‌کند؛ به همین دلیل دو photo server که یک کار را انجام می‌دهند همچنان به commandهای متفاوتی برای backup نیاز دارند. بنابراین، پیش از آن‌که به یک dump اعتماد کنید، بررسی کنید app شما چه داده‌هایی را ذخیره می‌کند. در انتهای دیگر این طیف، نمونه‌ای مانند یک library در Jellyfin که مانند یک video store در دهه 90 بازسازی شده است قرار دارد؛ این سرویس کل catalogue خود را از service دیگری می‌خواند و در نتیجه معمولاً فقط به کپی‌کردن configuration خودش نیاز دارد. در مقابل، mem0 به backup هر دو database نیاز دارد؛ در غیر این صورت restore قابل استفاده نخواهد بود.

اگر فقط 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 به این معنی است که درخواست هیچ‌کدام از آن‌ها را نداشته است. حافظه باید به چیزی محدود (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 جدید استفاده کنید.

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

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

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

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

error: port 3000 is already in use از سمت target make up در مخزن می‌آید که وقتی پورت‌های 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 check) که در بسته موجود است نیز با موفقیت انجام می‌شود، زیرا ارائه‌دهنده در واقع همان 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 جدید به ذخیره‌ساز برداری اختصاص دهید.