Claude Code

Self-hosted memory for Claude Code with mem0

October 1, 2026 · 5 min read

Claude Code ships with a built-in memory system, but it’s markdown files scoped to a project directory. I wanted one durable memory that follows me across every project and session, stays entirely on my machine, and doesn’t depend on a hosted memory service. Here’s the stack that does it — and the potholes hit along the way.

The shape of it

┌─────────────┐   SSE over HTTP   ┌───────────────┐   SQL   ┌──────────────┐
│ Claude Code │ ────────────────▶ │ mem0-open-mcp │ ──────▶ │ pgvector     │
│ (MCP client │  :8765 + bearer   │  (Docker)     │         │  (Docker)    │
│  + CLAUDE.md)│                  │  mem0ai lib   │         │  mem0_claude │
└─────────────┘                   └──────┬────────┘         └──────────────┘
                                         │ OpenAI-compatible API
                                         ▼ host.docker.internal:9800/v1
                                  ┌───────────────┐
                                  │ oMLX (macOS)  │
                                  │ chat + embed  │
                                  └───────────────┘

Three moving parts: a model server, a database, and the MCP server. Each is boring on its own.

1. oMLX serves the models

mem0 makes two kinds of model calls: an LLM decides what’s worth remembering (extracting facts from conversation), and an embedder vectorizes memories for retrieval. Both run locally through oMLX, an OpenAI-compatible inference server for Apple Silicon:

Because it speaks the standard /v1 API, mem0’s openai provider works against it unchanged. Docker containers reach it at http://host.docker.internal:9800/v1 — Docker Desktop’s alias for the host machine.

2. pgvector stores the memories

A local clone of the mem0 repo provides a compose project (server/docker-compose.yaml) whose postgres service is pgvector/pgvector:pg17. The full mem0 API server and dashboard from that compose file aren’t needed — the MCP server talks to Postgres directly through the mem0ai Python library — so only the database container runs.

An init script mounted into /docker-entrypoint-initdb.d/ creates a dedicated database and user on first boot, so Claude’s memories don’t share a schema with anything else using the same Postgres:

#!/bin/bash
set -e
psql -v ON_ERROR_STOP=1 -U "$POSTGRES_USER" -d "$POSTGRES_DB" <<'EOSQL'
   CREATE USER mem0_claude_user WITH PASSWORD '<db-password>' SUPERUSER;
EOSQL
createdb -U "$POSTGRES_USER" mem0_claude -O mem0_claude_user || true
psql -v ON_ERROR_STOP=1 -U "$POSTGRES_USER" -d mem0_claude <<'EOSQL'
   CREATE EXTENSION IF NOT EXISTS vector;
EOSQL

Scripts in that directory run in alphabetical order — the zz- prefix makes this one run after the repo’s own init script.

3. mem0-open-mcp is the bridge

mem0-open-mcp is the open-source MCP server that exposes mem0 as tools: add_memories, search_memory, list_memories, and friends. It runs as a small custom image:

FROM python:3.11-slim
WORKDIR /app
RUN pip install --no-cache-dir "mem0ai<2" "mem0-open-mcp" "mcp>=1.28,<2" "psycopg[binary,pool]"

# Patch a real bug in 0.2.12: AsyncMemory.from_config is not a coroutine,
# so awaiting it crashes the server during startup.
RUN sed -i 's/await AsyncMemory\.from_config/AsyncMemory.from_config/g' \
        /usr/local/lib/python3.11/site-packages/mem0_server/server.py \
        /usr/local/lib/python3.11/site-packages/mem0_server/cli.py

EXPOSE 8765
CMD ["mem0-open-mcp", "serve"]

All behavior lives in a bind-mounted YAML, so config edits never require a rebuild:

server:
  host: "0.0.0.0"
  port: 8765
  user_id: "claude_user_01"

auth:
  api_key: "<bearer-token-clients-must-send>"

llm:
  provider: "openai"
  config:
    base_url: "http://host.docker.internal:9800/v1"
    model: "gemma-4-e2b-it-4bit"
    api_key: "env:OPENAI_API_KEY"

embedder:
  provider: "openai"
  config:
    base_url: "http://host.docker.internal:9800/v1"
    model: "bge-m3-mlx-8bit"
    api_key: "env:OPENAI_API_KEY"
    embedding_dims: 1024

vector_store:
  provider: "pgvector"
  config:
    host: "postgres"        # compose service name, resolved on the docker network
    port: 5432
    collection_name: "claude_memories"
    embedding_model_dims: 1024
    extra:
      dbname: "mem0_claude"
      user: "mem0_claude_user"
      password: "<db-password>"

A compose override in the mem0 clone adds the service to the same network as Postgres:

services:
  mem0-open-mcp:
    build:
      context: ~/.config/mem0-claude
      dockerfile: Dockerfile
    image: mem0-open-mcp:local
    restart: unless-stopped
    volumes:
      - ~/.config/mem0-claude/mem0-open-mcp.yaml:/app/mem0-open-mcp.yaml:ro
    environment:
      - OPENAI_API_KEY=${OPENAI_API_KEY}
    networks:
      - mem0_network
    ports:
      - "8765:8765"
    command: mem0-open-mcp serve --port 8765 --user-id claude_user_01
    depends_on:
      - postgres

4. Teaching Claude Code to use it

Register the server at user scope so every project sees it:

claude mcp add --scope user --transport sse mem0 \
  http://127.0.0.1:8765/mcp/claude/sse/claude_user_01 \
  --header "Authorization: Bearer <bearer-token>"

The URL path encodes the client name and user id; the bearer token must match auth.api_key in the YAML.

Tools existing isn’t enough — Claude also needs to know when to call them. My global ~/.claude/CLAUDE.md says, in effect: mem0 is the primary memory store — save durable preferences and context there, search it when past sessions would help, and fall back to the file-based memory only when the server is unreachable. Without that paragraph, the server just sits there unused.

Gotchas

Checking it works

claude mcp list                      # mem0 should show ✔ Connected
docker ps | grep -E 'mem0|postgres'  # both containers up

Then the real test: tell Claude “remember that I prefer X”, start a new session, and ask what it knows about you. If the answer comes back, the loop is closed.

← Back to all posts