SSD Nodes Learn 🎉 VPS desde $4.99/mes
Guías Matt ConnorPor Matt Connor · Actualizado 2026-08-07

Alojar mem0 en un VPS: RAM y despliegue local

Descubre la RAM real de mem0, el Compose ligado a localhost, TLS para la API y el límite de 8 GB si ejecutas Ollama con un modelo 8B local.

Coste real de alojar mem0 en un VPS en cuanto a RAM

Alojar mem0 por cuenta propia implica ejecutar tres contenedores: el servidor de memoria FastAPI, Postgres con la extensión pgvector y un panel Next.js. mem0 es una capa de memoria para agentes. Se le envía una conversación, un modelo de lenguaje extrae los hechos persistentes de esa conversación y esos hechos se almacenan como vectores para que una consulta posterior pueda recuperar los relevantes.

Reserve aproximadamente 1 GB de memoria residente para los tres contenedores y entre 3 y 4 GB de disco una vez creadas las imágenes. Un VPS de 2 GB ejecuta esta configuración sin problemas cuando el modelo de lenguaje está en otro lugar. Cuando el modelo se ejecuta en el mismo equipo mediante Ollama, el modelo domina el consumo: un modelo de 8B cuantizado a 4 bits necesita aproximadamente 6 GB por sí solo, por lo que la configuración completamente local empieza en 8 GB.

No tome esas cifras de una publicación de blog, incluida esta. Mida la pila que realmente ha creado.

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

docker stats muestra la memoria residente de cada contenedor. docker system df -v muestra el espacio de disco que ocupa cada imagen y cada volumen.

El estado estable no representa el pico. docker compose up -d --build compila el panel Next.js, y esa compilación de Node es el momento de mayor consumo de toda la instalación. En un VPS de 1 GB, el kernel detiene el proceso mediante el asesino de procesos por falta de memoria y la compilación termina con exit code 137. Confirme la causa antes de buscar un error de Docker:

dmesg -T | grep -i "killed process"

Si un servidor le parece demasiada infraestructura para lo que necesita, existen opciones más pequeñas. un almacén local de memoria para agentes sin ningún servidor y memoria integrada en Claude Code omiten la base de datos. Vuelva aquí cuando varios agentes o varios equipos necesiten leer las mismas memorias.

¿Necesito Neo4j para la memoria de grafos de mem0?

No. Si una guía indica que debe añadir un contenedor de Neo4j, esa guía es anterior al código actual.

La memoria de grafos de mem0 significaba antes una base de datos de grafos externa, configurada mediante una clave graph_store con enable_graph establecido en true. El nuevo algoritmo de memoria, incluido en abril de 2026, eliminó ambas claves del SDK de código abierto. La extracción de entidades ahora se ejecuta dentro de la ruta normal de adición, y las entidades se escriben en una segunda colección de pgvector cuyo nombre se obtiene añadiendo _entities al nombre de la colección principal. No es necesario ejecutar ninguna migración. La vinculación de entidades integrada empieza a funcionar en la siguiente llamada de adición.

Eliminar el almacén de grafos evita un contenedor JVM, su heap y varios cientos de megabytes de imagen. En un VPS de 2 GB, esa diferencia puede determinar si el sistema funciona o empieza a usar swap.

Esto es lo que se pierde, de forma directa. Los resultados de búsqueda incluían antes un campo relations con la lista de relaciones entre entidades. Ese campo ya no existe. Las coincidencias de entidades ahora aumentan la posición de una memoria en la puntuación combinada, pero no hay ninguna estructura que se pueda recorrer. Si la aplicación recorría esas relaciones, mem0 ya no las conserva. En ese caso, debe mantener una base de datos de grafos propia fuera de mem0 y alimentarla con su propio código.

El archivo compose del repositorio es una configuración compose de desarrollo

server/docker-compose.yaml declara name: mem0-dev y lo hace realmente. Léalo antes de ejecutarlo, porque contiene cinco configuraciones incorrectas para un servidor.

  • Compila desde server/dev.Dockerfile y monta su copia de trabajo sobre la imagen mediante .:/app. Por tanto, el contenedor ejecuta lo que haya en ese directorio en lugar de lo que compiló.
  • Su comando es rm -rf /app/packages && pip install -q --force-reinstall --no-deps mem0ai && alembic upgrade head && uvicorn main:app --reload. Esto reinstala mem0ai desde PyPI cada vez que se inicia el contenedor. Por tanto, la versión que ejecuta el servidor puede cambiar durante un reinicio que no pretendía que fuera una actualización.
  • El mismo paso de pip hace que un reinicio sin red saliente falle antes de que se ejecute uvicorn. En ese caso, el servidor de memoria queda detenido porque no se puede acceder a PyPI.
  • --reload inicia el observador de archivos de uvicorn. Su función es reiniciar el proceso cuando se modifica el código. En producción consume memoria y crea un segundo proceso sin aportar ninguna utilidad. El Dockerfile de producción también incluye --reload en su CMD, por lo que debe sobrescribir el comando en cualquiera de los dos casos.
  • Los puertos publicados son "8888:8000", "8432:5432" y "3000:3000". Un puerto publicado sin una dirección delante se enlaza a 0.0.0.0. Por tanto, Postgres queda accesible desde Internet en 8432 en cuanto se inicia la pila.

Este último punto requiere una advertencia aparte. Docker publica un puerto escribiendo sus propias reglas delante de la cadena que administra ufw. Por tanto, ufw deny 8432 no cierra un puerto de contenedor publicado. Publicar puertos de Docker directamente sin pasar por ufw explica las reglas implicadas.

Un archivo de Compose para un servidor real

Trabaje dentro de server/, mantenga init-db.sh donde está y sustituya docker-compose.yaml por esto.

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

Aquí importan cinco cambios, y cada uno tiene un motivo.

Cada entrada ports comienza con 127.0.0.1, por lo que el kernel acepta esas conexiones sólo desde el propio servidor. Todo lo externo llega a través del reverse proxy, que es el único componente que almacena un certificado.

Postgres no tiene ningún bloque ports. El contenedor mem0 accede a él mediante mem0_network usando el nombre del servicio, por lo que publicar 8432 no aporta nada y deja un puerto abierto. Use docker compose exec postgres psql -U postgres cuando necesite un shell.

El historial pasa del bind mount ./history a un volumen con nombre. Un bind mount vincula los datos a una ruta y un uid concretos de este host, mientras que un volumen con nombre es un objeto que Docker puede incluir en una instantánea y mover. Volúmenes con nombre frente a bind mounts explica cuándo conviene usar cada uno.

El comando elimina --reload y conserva alembic upgrade head. Mantenga ese paso de migración. Sin él, la aplicación se inicia contra una base de datos sin tablas y todas las peticiones fallan en la primera consulta.

NEXT_PUBLIC_API_URL es la URL a la que accede el navegador, por lo que debe ser la dirección HTTPS pública y no http://mem0:8000. Next.js incluye en línea todos los valores NEXT_PUBLIC_ durante la compilación, por lo que cambiarlo requiere docker compose up -d --build mem0-dashboard. Un simple reinicio conserva el valor anterior integrado en JavaScript y el dashboard realiza las peticiones al host incorrecto.

Los secretos están en .env, y .env no debe exponerse a Internet

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

Configure POSTGRES_PASSWORD, JWT_SECRET y ADMIN_API_KEY. Deje AUTH_DISABLED=false sin configurar. El nombre describe claramente lo que hace esa opción: cuando está activada, el servidor entrega toda la memoria que contiene a cualquiera que pueda acceder al puerto. Configure MEM0_TELEMETRY=false si no quiere que el evento de incorporación se envíe al sistema ascendente.

ADMIN_API_KEY se compara con la cabecera X-API-Key mediante secrets.compare_digest, y una coincidencia omite todas las consultas a la base de datos. Es una credencial de root para toda la API. Trátela como tal: no la deje en el historial del shell, no la guarde en git ni la pegue en un indicador de comandos. Archivos de entorno de Compose y dónde se filtran los secretos y cómo evitar que las claves de API entren en el contexto de un agente se aplican directamente, porque los clientes de este servidor son agentes.

Los valores cargados desde env_file quedan en el entorno del contenedor, y docker inspect los muestra completos. Cualquier usuario del grupo docker puede leerlos, y cualquier usuario del grupo docker tiene de hecho privilegios de root en el host.

Coloque TLS delante de la API en lugar de abrir 8888

La API responde en 127.0.0.1:8888 y el dashboard en 127.0.0.1:3000. nginx termina TLS (seguridad de la capa de transporte) en 443 y reenvía las peticiones a ambos servicios.

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 es más importante de lo que parece. Una llamada de adición se bloquea mientras el modelo de lenguaje lee la conversación y extrae los datos. Un modelo local de 8B ejecutándose en la CPU tarda habitualmente más que el valor predeterminado de 60 segundos de nginx. Entonces, el cliente recibe 504 Gateway Time-out mientras el modelo sigue procesando y la memoria se sigue guardando. El resultado es que se crea una memoria aunque se haya informado de que la operación falló.

Cierre el resto con una política ufw predeterminada de denegación, dejando abiertos 22 y 443. Emita el certificado con certbot en Ubuntu 24.04 detrás de nginx. Si el servidor ya publica otras aplicaciones mediante Traefik para enrutar varias aplicaciones de Compose, añada mem0 a ese router en lugar de instalar un segundo proxy.

Prueba básica: añadir una memoria y leerla de nuevo

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"}'

Una respuesta correcta es un objeto JSON con una lista results. Cada entrada contiene un id, el texto memory extraído y "event": "ADD". El algoritmo actual sólo devuelve eventos ADD. Los eventos UPDATE y DELETE se eliminaron, por lo que su ausencia no es un error.

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}'

El dato sobre Postgres 17 debe devolverse con una puntuación. Pase el identificador dentro de filters, como se muestra. Un user_id de nivel superior sigue funcionando, y el servidor registra Top-level user_id in /search is deprecated. Use filters={...} instead. cada vez que lo usa.

Limpie los datos de prueba para que no contaminen las búsquedas reales:

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

Si la búsqueda devuelve menos filas de las esperadas, compruebe los valores predeterminados antes de atribuirlo a la recuperación. En la versión actual, top_k tiene el valor predeterminado 20, en lugar de 100, y threshold tiene el valor predeterminado 0.1 en lugar de no tener ninguno, por lo que los resultados débiles se filtran automáticamente. Cuando esto funcione con curl, esos mismos endpoints son los que debe conectar a un agente, ya sea directamente o mediante un servidor MCP que se ejecute en el mismo VPS.

Ejecutar mem0 sin ninguna clave de OpenAI

Empiece por el bloqueo, porque aparecerá durante los primeros cinco minutos. La imagen del servidor incluye un conjunto fijo de bibliotecas de proveedores, y /configure rechaza cualquier elemento que no esté en él:

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.

No tiene que volver a compilar nada. Ollama ofrece una API compatible con OpenAI en /v1, que admite /v1/chat/completions y /v1/embeddings, y el proveedor de OpenAI de mem0 acepta un openai_base_url. Apunte esa clave a Ollama y la comprobación incluida se ejecutará correctamente, porque el proveedor realmente es openai. Sólo cambia la dirección.

Añada Ollama al mismo proyecto de Compose:

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

Añada ollama_models: bajo la clave de nivel superior volumes: y, después, descargue un modelo de chat y un modelo de embeddings:

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

Si Ollama ya se ejecuta en el host como una unidad de systemd, como se describe en ejecutar Ollama directamente en un VPS, no apunte el contenedor a 127.0.0.1:11434. Dentro del contenedor de mem0, 127.0.0.1 es el contenedor de mem0. Asigne al servicio de mem0 extra_hosts: ["host.docker.internal:host-gateway"], establezca Environment="OLLAMA_HOST=0.0.0.0:11434" en un drop-in de systemd para que Ollama escuche en una dirección accesible desde el bridge y mantenga cerrado 11434 en el firewall.

Pregunte al modelo cuál es su dimensión de embeddings antes de configurar nada

Este paso determina si la recuperación funcionará.

El almacén pgvector de mem0 crea su tabla con un ancho de vector fijo, vector vector(1536), porque embedding_model_dims tiene 1536 como valor predeterminado, el ancho de text-embedding-3-small de OpenAI. nomic-embed-text devuelve 768 valores. Nada dentro de mem0 compara esos dos números, por lo que la discrepancia aparece en Postgres durante la primera inserción:

expected 1536 dimensions, not 768

Tampoco confíe en el número de este párrafo. Pregunte al modelo:

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

Esto muestra el ancho que debe usar su colección. Escriba la configuración en un archivo, porque pegar una contraseña de Postgres mediante el entrecomillado del shell es una forma de introducir errores tipográficos en producción.

{
  "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"

La segunda llamada vuelve a leer la configuración. Esta es la comprobación de que la escritura se realizó correctamente. Después, repita la prueba de humo anterior.

Hay cuatro detalles de ese JSON que no son evidentes. Cada uno provoca un fallo si se configura incorrectamente.

api_key es la cadena ollama y Ollama ignora su valor. No puede estar vacío, porque la biblioteca cliente de OpenAI genera un error antes de que salga ninguna solicitud del proceso si no se establece una clave. Cualquier cadena no vacía funciona.

embedding_model_dims va en el almacén de vectores y deliberadamente no hay ningún embedding_dims en el embedder. mem0 sólo envía el parámetro dimensions de OpenAI cuando se establece embedding_dims, y los backends que no implementan el truncamiento Matryoshka rechazan directamente ese parámetro. Establezca el ancho al crear la tabla y no modifique el embedder.

collection_name es nuevo. mem0 crea su tabla con CREATE TABLE IF NOT EXISTS, por lo que apuntar un ancho diferente a una colección existente no hace nada: la columna vector(1536) antigua permanece y cada inserción falla. Un cambio de ancho requiere un nombre de colección nuevo o eliminar manualmente la tabla antigua.

El host de openai_base_url es el nombre del servicio de Compose, ollama, no localhost. Los contenedores se resuelven entre sí mediante el nombre del servicio en su red compartida.

El coste de usar una ruta totalmente local

Sea realista sobre la calidad. Las puntuaciones de las pruebas de referencia publicadas por mem0 se midieron con modelos de vanguardia para realizar la extracción. Considérelas un límite superior, no una previsión para un modelo 8B en su VPS. Un modelo pequeño escribe hechos más vagos y, en ocasiones, devuelve texto cuando se solicitó JSON. Esto aparece como una llamada add que devuelve una lista results vacía sin ningún error.

La velocidad es el otro coste. La extracción usando sólo la CPU tarda varios segundos por llamada add y cada mensaje almacenado tiene ese coste. Si esa latencia es importante, un VPS con una GPU conectada es la solución adecuada. Añadir más núcleos de CPU a un modelo 8B ayuda mucho menos de lo que suele esperarse.

Se aplica una regla independientemente de la opción elegida: no mezcle modelos de embeddings dentro de una misma colección. Dos modelos diferentes que casualmente compartan el mismo ancho producen vectores no comparables. La inserción se completa, la búsqueda devuelve filas y las filas son incorrectas, sin que nada informe del error.

Backups: hay dos bases de datos, no una

El error más común al hacer copias de seguridad de mem0 es volcar una sola base de datos. init-db.sh crea mem0_app junto a la base de datos predeterminada postgres, y cada una contiene datos distintos. La base de datos postgres contiene las colecciones de pgvector, que son las memorias. mem0_app contiene los usuarios, las sesiones, las claves de API y los registros de solicitudes.

Si restaura sólo postgres, las memorias reaparecen, pero se pierden todas las cuentas y las claves de API. Por tanto, nada puede autenticarse para leerlas. Vuelque ambas bases de datos, junto con los roles, en un solo comando:

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

El volumen de historial es independiente de Postgres y necesita su propia copia:

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

Docker antepone el nombre del proyecto a los nombres de los volúmenes. Confirme el nombre con docker volume ls antes de dar por supuesto mem0_mem0_history.

Restaure los datos en un contenedor temporal y compruebe el número de filas antes de confiar en la copia:

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

Una copia de seguridad que nunca se ha restaurado es sólo una suposición. Cuando los volcados sean correctos, cópielos fuera del servidor con snapshots de restic en almacenamiento externo, porque una copia de seguridad almacenada en el mismo servidor que protege no protege nada.

Modos de fallo y las cadenas exactas que verá

{"detail":"Authentication required. Provide a Bearer token or X-API-Key header."} significa que falta la cabecera o que está mal escrita. El nombre es X-API-Key, y curl envía los nombres de las cabeceras literalmente.

{"detail":"At least one identifier (user_id, agent_id, run_id) is required."} en una operación de adición significa que la solicitud no incluía ninguna. Una memoria debe estar asociada a algo, porque los filtros de búsqueda se aplican exactamente a esos campos.

LLM provider 'ollama' is not bundled in this image con HTTP 400 significa que envió "provider": "ollama". Use "provider": "openai" con openai_base_url apuntando a Ollama.

expected 1536 dimensions, not 768 de Postgres significa que la colección se creó con una dimensión y el embedder devuelve otra. Establezca embedding_model_dims en el almacén vectorial y use un collection_name nuevo.

La búsqueda devuelve filas que no tienen sentido después de cambiar de modelo, sin ningún error. La dimensión sigue coincidiendo, por lo que la base de datos funciona correctamente, pero dos modelos colocan la misma oración en posiciones distintas. Cree una colección nueva y vuelva a añadir los datos.

Connection refused en los registros de mem0 al conectarse con Ollama suele significar 127.0.0.1 en openai_base_url. Dentro del contenedor, esa dirección corresponde al propio contenedor. Use el nombre del servicio o la puerta de enlace del host cuando Ollama se ejecute en el host.

504 Gateway Time-out de nginx durante una operación de adición significa que el modelo tardó más de proxy_read_timeout. Aumente ese valor y compruebe si la memoria se escribió antes de volver a intentar la solicitud.

exit code 137 durante docker compose up --build indica que el asesino por falta de memoria detuvo la compilación del dashboard. Añada swap o compile la imagen en una máquina más grande y súbala a un registro.

error: port 3000 is already in use procede del destino make up del repositorio, que se niega a iniciarse cuando los puertos 3000 o 8888 están ocupados. Busque el proceso propietario con lsof -iTCP:3000 -sTCP:LISTEN.

FAQ

¿Sigo necesitando Neo4j para ejecutar mem0 con memoria de grafos?

No. El nuevo algoritmo de memoria, publicado en abril de 2026, eliminó las claves de configuración graph_store y enable_graph del SDK de código abierto. La extracción de entidades se ejecuta ahora durante una operación normal de adición y escribe en una segunda colección de pgvector llamada <collection_name>_entities. Por tanto, no se necesita una base de datos de grafos externa, ningún contenedor adicional ni un paso de migración. La contrapartida es que el campo relations ya no existe en los resultados de búsqueda. Las entidades ahora aumentan la clasificación de una memoria en lugar de proporcionar aristas que recorrer. Por ello, una aplicación que recorría esas relaciones necesita su propio almacén de grafos fuera de mem0.

¿Cuál es el VPS más pequeño que puede ejecutar un servidor mem0 autoalojado?

Si el modelo de lenguaje está alojado en otro lugar, 2 GB de RAM y unos 4 GB de espacio libre en disco bastan para el contenedor de la API, Postgres y el dashboard. El momento más exigente es la primera compilación, porque compilar el dashboard de Next.js usa más memoria que ejecutarlo. En un equipo con 1 GB, la compilación termina con exit code 137. Si Ollama se ejecuta en el mismo servidor, dimensione el sistema según el modelo. Un modelo 8B con cuantización de 4 bits necesita aproximadamente 6 GB por sí solo, así que debe planificar 8 GB.

¿Puedo ejecutar mem0 sin una clave de API de OpenAI?

Sí, mediante el endpoint compatible con OpenAI de Ollama. Definir "provider": "ollama" falla porque la imagen del servidor sólo incluye las bibliotecas openai, anthropic y gemini, y devuelve HTTP 400. En su lugar, mantenga "provider": "openai" y defina "openai_base_url": "http://ollama:11434/v1" con cualquier api_key no vacío, tanto para el llm como para el embedder. Ollama ignora la clave y la comprobación del proveedor incluida funciona porque el proveedor es realmente openai.

¿Por qué mem0 no devuelve resultados después de cambiar a un modelo local de embeddings?

Porque la tabla de pgvector se creó con una dimensión fija. embedding_model_dims tiene 1536 de forma predeterminada, nomic-embed-text devuelve 768 y Postgres rechaza la inserción con expected 1536 dimensions, not 768. mem0 crea la tabla con CREATE TABLE IF NOT EXISTS, por lo que cambiar sólo el número no modifica una colección existente. Defina embedding_model_dims con la dimensión real de su modelo, confirme esa dimensión llamando a /v1/embeddings y contando los valores que devuelve, y asigne al almacén vectorial un nuevo collection_name al mismo tiempo.