SSD Nodes Learn 🎉 VPS من $4.99/شهر
الأدلة Matt Connorبقلم Matt Connor

استضافة mem0 ذاتياً على VPS: RAM وDocker وOllama

شغّل mem0 على VPS مع رقم واقعي للذاكرة: نحو 1 GB للحاويات، و8 GB عند تشغيل Ollama محلياً، مع Compose وTLS وربط API بـ localhost.

ما تكلّفه استضافة mem0 ذاتياً على VPS من RAM فعلياً

تعني استضافة mem0 ذاتياً تشغيل ثلاث حاويات: خادم الذاكرة FastAPI، وPostgres مع إضافة pgvector، ولوحة تحكم Next.js. يعمل mem0 كطبقة ذاكرة للوكلاء. ترسل إليه محادثة، ويستخرج نموذج لغوي الحقائق الدائمة من تلك المحادثة، ثم تُخزَّن هذه الحقائق كمتجهات كي يعيد استعلام لاحق الحقائق ذات الصلة.

خصّص نحو 1 GB من الذاكرة المقيمة للحاويات الثلاث، ونحو 3 إلى 4 GB من مساحة القرص بعد إنشاء الصور. يشغّل VPS بسعة 2 GB هذه المكونات بشكل مريح عندما يكون النموذج اللغوي في مكان آخر. أما عند تشغيل النموذج على الخادم نفسه عبر Ollama، فيستهلك النموذج معظم الموارد: إذ يحتاج نموذج 8B مكمَّم إلى 4 بتات إلى نحو 6 GB بمفرده، ولذلك يبدأ التشغيل المحلي الكامل من 8 GB.

لا تعتمد هذه الأرقام على تدوينة، بما في ذلك هذه التدوينة. قِس الحزمة التي أنشأتها فعلياً.

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

يعرض docker stats الذاكرة المقيمة لكل حاوية. ويعرض docker system df -v مساحة القرص التي تشغلها كل صورة وكل volume.

لا تمثل حالة الاستقرار ذروة الاستهلاك. يترجم docker compose up -d --build لوحة تحكم Next.js، ويكون بناء Node هذا أكثر مراحل التثبيت استهلاكاً للذاكرة. على VPS بسعة 1 GB، يوقفه قاتل نفاد الذاكرة في النواة، وينتهي البناء بالرسالة exit code 137. أكّد السبب قبل البحث عن خطأ في Docker:

dmesg -T | grep -i "killed process"

إذا بدا الخادم أكبر من احتياجك، فهناك خيارات أصغر فعلاً. يتجاوز كل من مخزن ذاكرة محلياً للوكيل من دون خادم إطلاقاً وذاكرة تعمل داخل Claude Code نفسه قاعدة البيانات. عُد إلى هذا الخيار عندما تحتاج عدة وكلاء أو عدة أجهزة إلى قراءة الذكريات نفسها.

هل أحتاج إلى Neo4j لذاكرة الرسوم البيانية في mem0؟

لا. إذا أخبرك دليل بإضافة حاوية Neo4j، فهذا الدليل أقدم من الشيفرة الحالية.

كانت ذاكرة الرسوم البيانية في mem0 تعني سابقاً استخدام قاعدة بيانات رسوم بيانية خارجية، تُضبط ضمن مفتاح graph_store مع تعيين enable_graph إلى true. أزالت خوارزمية الذاكرة الجديدة، التي أُصدرت في April 2026، كلا المفتاحين من SDK مفتوح المصدر. يجري الآن استخراج الكيانات داخل مسار الإضافة العادي، وتُكتب الكيانات في مجموعة pgvector ثانية تحمل اسم المجموعة الرئيسية نفسه مع إلحاق _entities به. لا توجد عملية ترحيل يجب تشغيلها. ويبدأ ربط الكيانات المضمّن بالعمل عند استدعاء الإضافة التالي.

يؤدي حذف مخزن الرسوم البيانية إلى الاستغناء عن حاوية JVM وذاكرة heap الخاصة بها، وعن عدة مئات من الميغابايتات من صورة الحاوية. وعلى VPS بسعة 2 GB، قد يكون ذلك الفارق بين التشغيل واللجوء إلى swap.

إليك ما ستفقده بوضوح. كانت نتائج البحث تتضمن سابقاً الحقل relations الذي يسرد الروابط بين الكيانات. اختفى هذا الحقل. تؤدي مطابقات الكيانات الآن إلى رفع موضع الذاكرة في النتيجة الموحّدة، ولا توجد بنية يمكنك اجتيازها. إذا كان تطبيقك يستعرض تلك العلاقات، فلن يحتفظ mem0 بها بعد الآن. وعليك الاحتفاظ بقاعدة بيانات رسوم بيانية خاصة بك خارج mem0، وتغذيتها بواسطة شيفرتك الخاصة.

ملف compose الموجود في المستودع مخصص لبيئة التطوير

يعرّف server/docker-compose.yaml قيمة name: mem0-dev، وهو يطبّقها فعلاً. اقرأه قبل تشغيله، لأن فيه خمسة أمور غير مناسبة للخادم.

  • يبني من server/dev.Dockerfile، ويربط نسخة العمل المحلية فوق الصورة باستخدام .:/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 مراقب الملفات الخاص بـuvicorn. وُجد هذا المراقب لإعادة تشغيل العملية عند تعديل الشيفرة. لكنه يستهلك الذاكرة ويشغّل عملية ثانية لا تؤدي وظيفة مفيدة في الإنتاج. ويتضمن Dockerfile المخصص للإنتاج --reload في CMD أيضاً، لذلك يجب تجاوز الأمر في كلتا الحالتين.
  • المنافذ المنشورة هي "8888:8000" و"8432:5432" و"3000:3000". عندما لا يسبق المنفذ المنشور عنوان، فإنه يرتبط بـ0.0.0.0. لذلك يستجيب 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

هناك خمسة تغييرات مهمة هنا، ولكل واحد منها سبب.

يبدأ كل إدخال ports بـ 127.0.0.1، لذلك لا تقبل النواة هذه الاتصالات إلا من الخادم نفسه. يمر كل ما يأتي من الخارج عبر الـreverse proxy، وهو المكوّن الوحيد الذي يحتفظ بشهادة.

لا يحتوي Postgres على كتلة ports إطلاقاً. تصل حاوية mem0 إليه عبر mem0_network باستخدام اسم الخدمة، لذلك لا يحقق نشر المنفذ 8432 أي فائدة، بل يترك منفذاً مفتوحاً. استخدم docker compose exec postgres psql -U postgres عندما تحتاج إلى shell.

ينتقل السجل من bind mount في ./history إلى volume مسمّى. يربط bind mount البيانات بمسار واحد وuid واحد على هذا الخادم، بينما يكون volume المسمّى كائناً يستطيع Docker أخذ snapshot له ونقله. يوضّح الـvolumes المسمّاة مقارنةً بـbind mounts متى يكون كل خيار مناسباً.

يحذف الأمر --reload ويُبقي alembic upgrade head. أبقِ خطوة الترحيل هذه. من دونها يبدأ التطبيق باستخدام قاعدة بيانات بلا جداول، ويفشل كل طلب عند أول استعلام.

NEXT_PUBLIC_API_URL هو عنوان URL الذي يستدعيه متصفحك، لذلك يجب أن يكون عنوان HTTPS العام، وليس http://mem0:8000. يضمّن Next.js كل قيمة NEXT_PUBLIC_ في وقت البناء، لذلك يتطلب تغييرها تنفيذ docker compose up -d --build mem0-dashboard. تؤدي إعادة التشغيل العادية إلى إبقاء القيمة القديمة مضمّنة في JavaScript، فيستدعي dashboard المضيف الخطأ.

الأسرار موجودة في .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 دون ضبط. يوضّح الاسم وظيفة ذلك الخيار بدقة: عند تفعيله، يسلّم الخادم كل ذاكرة يحتفظ بها إلى أي طرف يستطيع الوصول إلى المنفذ. اضبط MEM0_TELEMETRY=false إذا كنت لا تريد إرسال حدث الإعداد الأولي إلى الجهة upstream.

تتم مقارنة ADMIN_API_KEY مع ترويسة X-API-Key باستخدام secrets.compare_digest، وتؤدي المطابقة إلى تجاوز جميع عمليات البحث في قاعدة البيانات. هذا اعتماد root لواجهة API بأكملها. تعامل معه وفقاً لذلك: لا تضعه في سجل shell، ولا في git، ولا تلصقه في موجّه الأوامر. ينطبق كل من ملفات بيئة Compose والمواضع التي تتسرب منها الأسرار وإبعاد مفاتيح API عن سياق أحد الوكلاء مباشرة، لأن مستهلكي هذا الخادم هم وكلاء.

توجد القيم المحمّلة من env_file في بيئة الحاوية، ويطبعها docker inspect بالكامل. يستطيع أي شخص في المجموعة docker قراءتها، وأي شخص في المجموعة docker يملك فعلياً صلاحيات 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 أكثر مما يبدو. تستمر مكالمة الإضافة في الانتظار بينما يقرأ نموذج اللغة المحادثة ويستخرج الحقائق. يستغرق نموذج محلي بحجم 8B يعمل على CPU وقتاً أطول بانتظام من المهلة الافتراضية البالغة 60 ثانية في nginx، وعندها يرى المستدعي 504 Gateway Time-out بينما يواصل النموذج عمله وتُكتب الذاكرة فعلياً. والنتيجة ذاكرة أُبلغتَ بأن كتابتها فشلت.

أغلق بقية المنافذ باستخدام سياسة ufw افتراضية تمنع كل شيء، مع إبقاء المنفذين 22 و443 مفتوحين. أصدر الشهادة باستخدام certbot على Ubuntu 24.04 خلف nginx. إذا كان الخادم يمرّر بالفعل تطبيقات أخرى عبر Traefik لتوجيه عدة تطبيقات Compose، فأضف mem0 إلى ذلك الموجّه بدلاً من تثبيت Proxy آخر.

اختبار دخاني: أضف ذاكرة واحدة واقرأها مجدداً

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، تكون نقاط النهاية نفسها هي التي تربطها بوكيل، سواء مباشرة أو عبر خادم 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، وسينجح الفحص المضمّن لأن المزوّد هو فعلياً 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:، ثم نزّل نموذج محادثة واحداً ونموذج تضمين واحداً:

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" في ملف إسقاط systemd حتى يستمع Ollama على عنوان يمكن لشبكة bridge الوصول إليه، واترك المنفذ 11434 مغلقاً في الجدار الناري.

اسأل النموذج عن بُعد التضمين قبل ضبط أي شيء

تحدد هذه الخطوة وحدها ما إذا كان الاسترجاع سيعمل أصلاً.

ينشئ مخزن 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 هو ما يؤدي إلى وصول الأخطاء الإملائية إلى بيئة الإنتاج.

{
  "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 في مخزن المتجهات، ولا توجد عمداً قيمة embedding_dims في أداة التضمين. يرسل mem0 معامل OpenAI dimensions فقط عند ضبط embedding_dims، وت####رفض الواجهات الخلفية التي لا تطبق اقتطاع Matryoshka هذا المعامل مباشرة. اضبط العرض عند إنشاء الجدول، واترك أداة التضمين من دون تغيير.

collection_name جديد. ينشئ mem0 جدوله باستخدام CREATE TABLE IF NOT EXISTS، لذلك لا يؤدي توجيه عرض مختلف إلى مجموعة موجودة إلى أي تغيير: يبقى عمود vector(1536) القديم، ويفشل كل إدراج. يتطلب تغيير العرض اسماً جديداً للمجموعة، أو حذف الجدول القديم يدوياً.

المضيف في openai_base_url هو اسم خدمة Compose، أي ollama، وليس localhost. تحل الحاويات أسماء بعضها بعضاً باستخدام اسم الخدمة على شبكتها المشتركة.

تكلفة المسار المحلي بالكامل

كن صريحاً مع نفسك بشأن الجودة. قِيست نتائج المعايير المنشورة لـ mem0 باستخدام نماذج متقدمة لتنفيذ الاستخراج، لذلك تعامل معها كسقف لا كتوقع لنموذج 8B على VPS لديك. يكتب النموذج الصغير حقائق أكثر غموضاً، وأحياناً يعيد نصاً نثرياً عندما يُطلب منه JSON، ويظهر ذلك في صورة استدعاء add يعيد قائمة results فارغة من دون خطأ.

السرعة تكلفة أخرى. يستغرق الاستخراج باستخدام CPU فقط ثواني لكل استدعاء add، وتتحمل كل رسالة تخزنها هذه التكلفة. إذا كان هذا التأخير مهماً، فإن VPS مزود بوحدة GPU هو الحل الصريح. إن إضافة أنوية CPU إلى نموذج 8B تساعد بدرجة أقل بكثير مما يتوقعه الناس.

تنطبق قاعدة واحدة مهما كان اختيارك: لا تخلط أبداً نماذج التضمين داخل مجموعة واحدة. ينتج نموذجان مختلفان يصادف أن لهما العرض نفسه متجهات غير قابلة للمقارنة. ينجح الإدراج، ويعيد البحث صفوفاً، لكن الصفوف تكون خاطئة من دون أن يبلغ أي شيء عن وجود خطأ.

النسخ الاحتياطية: توجد قاعدتا بيانات، وليست قاعدة واحدة

الخطأ الأكثر شيوعاً عند أخذ نسخة احتياطية من mem0 هو تفريغ قاعدة بيانات واحدة. ينشئ init-db.sh قاعدة mem0_app إلى جانب قاعدة postgres الافتراضية، وتحتوي كل منهما على بيانات مختلفة. تحتوي قاعدة postgres على مجموعات pgvector، وهي الذكريات. وتحتوي mem0_app على المستخدمين والجلسات ومفاتيح API وسجلات الطلبات.

إذا استعدت postgres فقط، فستعود الذكريات بينما تختفي كل الحسابات ومفاتيح API، ولذلك لن يتمكن أي شيء من المصادقة لقراءتها. أفرغ القاعدتين، مع الأدوار، باستخدام أمر واحد:

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

وحدة تخزين السجل منفصلة عن 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.

استعد النسخة في حاوية مؤقتة، وتحقّق من أعداد الصفوف قبل الوثوق بها:

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."} يعني أن الترويسة مفقودة أو تحتوي على خطأ إملائي. الاسم هو X-API-Key، ويرسل curl أسماء الترويسات حرفياً.

{"detail":"At least one identifier (user_id, agent_id, run_id) is required."} عند الإضافة يعني أن الطلب لم يتضمن أياً منها. يجب ربط الذاكرة بنطاق محدد، لأن البحث يرشّح النتائج وفق هذه الحقول نفسها تماماً.

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 في مخزن المتجهات، واستخدم collection_name جديداً.

يعيد البحث صفوفاً غير منطقية بعد تغيير النموذج، من دون ظهور أي خطأ. ما زال العرض متطابقاً، لذلك لا ترى قاعدة البيانات مشكلة، لكن النموذجين يضعان الجملة نفسها في موضعين مختلفين. أنشئ مجموعة جديدة وأعد الإضافة.

Connection refused في سجلات mem0 أثناء الوصول إلى Ollama يعني عادةً وجود 127.0.0.1 في openai_base_url. داخل الحاوية، يشير ذلك العنوان إلى الحاوية نفسها. استخدم اسم الخدمة، أو بوابة المضيف عندما يعمل Ollama على المضيف.

504 Gateway Time-out من nginx عند الإضافة يعني أن النموذج استغرق وقتاً أطول من proxy_read_timeout. ارفع القيمة، وتحقق مما إذا كانت الذاكرة قد كُتبت بالفعل قبل إعادة محاولة الطلب.

exit code 137 أثناء docker compose up --build هو قاتل نفاد الذاكرة الذي يوقف إنشاء لوحة المعلومات. أضف swap، أو أنشئ الصورة على جهاز أكبر ثم ادفعها إلى سجل.

error: port 3000 is already in use ناتج عن هدف make up في المستودع، الذي يرفض البدء عندما يكون المنفذان 3000 أو 8888 مستخدمين. اعثر على مالك المنفذ باستخدام lsof -iTCP:3000 -sTCP:LISTEN.

FAQ

هل ما زلت أحتاج إلى Neo4j لتشغيل mem0 مع ذاكرة بيانية؟

لا. أزالت خوارزمية الذاكرة الجديدة، التي أُصدرت في أبريل 2026، مفتاحَي الإعداد graph_store وenable_graph من SDK مفتوح المصدر. ويجري الآن استخراج الكيانات أثناء عملية add عادية، ثم تُكتب النتائج في مجموعة pgvector ثانية باسم <collection_name>_entities. لذلك لا توجد قاعدة بيانات بيانية خارجية، ولا حاوية إضافية، ولا خطوة ترحيل. المقابل هو أن الحقل relations لم يعد موجوداً في نتائج البحث. أصبحت الكيانات ترفع ترتيب الذاكرة بدلاً من توفير حواف يمكنك اجتيازها. لذلك يحتاج التطبيق الذي كان يتنقل بين هذه العلاقات إلى مخزن بياني خاص به خارج mem0.

ما أصغر VPS يمكنه تشغيل خادم mem0 مستضاف ذاتياً؟

إذا كان نموذج اللغة مستضافاً في مكان آخر، فإن ذاكرة RAM بسعة 2 GB ونحو 4 GB من مساحة القرص الحرة تكفي لحاوية API وPostgres ولوحة المعلومات. تظهر المشكلة أثناء عملية البناء الأولى، لأن تجميع لوحة Next.js يستهلك ذاكرة أكبر من تشغيلها. لذلك يفشل البناء على جهاز بسعة 1 GB ويُنهيه النظام باستخدام exit code 137. إذا كان Ollama يعمل على الخادم نفسه، فحدّد الحجم وفقاً للنموذج بدلاً من ذلك. يحتاج نموذج 8B مع تكميم 4-bit إلى نحو 6 GB بمفرده، لذا خطط لاستخدام 8 GB.

هل يمكنني تشغيل mem0 من دون مفتاح OpenAI API؟

نعم، عبر نقطة النهاية المتوافقة مع OpenAI في Ollama. يفشل ضبط "provider": "ollama" لأن صورة الخادم تتضمن مكتبات openai وanthropic وgemini فقط، وتعيد HTTP 400. بدلاً من ذلك، أبقِ "provider": "openai" مضبوطاً، واضبط "openai_base_url": "http://ollama:11434/v1" على أي api_key غير فارغ، لكل من llm وembedder. يتجاهل Ollama المفتاح، ويمر فحص المزوّد المضمّن لأن المزوّد هو openai فعلاً.

لماذا لا يعرض mem0 أي نتائج بعد التبديل إلى نموذج تضمين محلي؟

لأن جدول 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 جديداً لمخزن المتجهات.