← Back to all posts

Setting Up Mem0 OSS Memory for Hermes Agent

· Jacob E. Dawson

Hermes Agent already has a useful built-in memory system. It keeps curated facts in MEMORY.md and USER.md, injects them into the prompt at session start, and keeps them intentionally small so the agent doesn't drown itself in stale context. The tradeoff is obvious: bounded memory is clean, but it is not deep.

Hermes also supports external memory providers. The current lineup includes things like Honcho, OpenViking, Hindsight, Holographic, RetainDB, ByteRover, Supermemory, and Mem0. These providers run alongside the built-in Hermes memory rather than replacing it. The built-in files remain the small, high-signal profile; the external provider becomes the larger searchable memory layer.

I went with Mem0 OSS because it hits the right middle ground for my setup:

  • It gives Hermes semantic memory search instead of only prompt-injected notes.
  • It can extract and deduplicate useful facts from conversations automatically.
  • It can run fully self-hosted with my own LLM, embedder, and vector database.
  • It does not require a Mem0 Cloud API key.
  • It uses a boring stack I can inspect and repair: Docker, Postgres, pgvector, and Ollama.

This post is the working version of the setup I ended up with.

The Target Architecture

The working stack is:

Component Choice Why
Hermes memory provider mem0 Adds semantic long-term memory tools
Mem0 mode oss No Mem0 Cloud dependency
Extraction LLM Ollama llama3.1:8b Local and good enough for memory extraction
Embedder Ollama nomic-embed-text Local embedding model supported by Hermes' Mem0 path
Vector store pgvector on Postgres Easy to inspect, backup, and run in Docker

The official Hermes memory docs are worth reading first because they explain the split between built-in memory and external providers. The important detail is that external memory is additive. MEMORY.md and USER.md still matter. Mem0 gives Hermes tools like mem0_list, mem0_search, mem0_add, mem0_update, and mem0_delete, plus background sync.

The Mem0 integration docs describe two modes:

  • Platform mode: managed Mem0 Cloud, fastest to set up, needs MEM0_API_KEY.
  • OSS mode: self-hosted, no Mem0 Cloud key, using your own LLM, embedder, and vector store.

I wanted the second one. If I am self-hosting the agent, I don't want the memory layer to be the one part that quietly depends on a cloud service.

Installing the Python Bits

Hermes runs from its own virtualenv, so provider dependencies need to be available there. In the ideal case the Hermes setup wizard installs these for you, but I prefer being explicit when I am debugging infrastructure.

~/.hermes/hermes-agent/venv/bin/pip install \
  "mem0ai>=2.0.7,<3" \
  psycopg2-binary \
  ollama

I originally experimented with a Hugging Face TEI embedding container, but the simpler and more reliable working setup was Ollama for both extraction and embeddings. Fewer moving parts wins.

Docker Services

The two services I need are:

Here is the shape of the startup script. This is intentionally generic; do not paste secrets from a blog post into your real .env.

#!/usr/bin/env bash
set -euo pipefail

HERMES_HOME="${HERMES_HOME:-$HOME/.hermes}"
BASE="${DOCKER_BASE_FOLDER:-$HERMES_HOME/docker}"

PGDATA="$BASE/mem0/postgresql"
OLLAMA_DATA="$BASE/mem0/ollama"

mkdir -p "$PGDATA" "$OLLAMA_DATA"

docker network inspect hermes-mem0 >/dev/null 2>&1 \
  || docker network create hermes-mem0 >/dev/null

docker run -d \
  --name hermes-mem0-pgvector \
  --network hermes-mem0 \
  --restart unless-stopped \
  -e POSTGRES_USER=postgres \
  -e POSTGRES_PASSWORD="$POSTGRES_ROOT_PASSWORD" \
  -e POSTGRES_DB="$POSTGRES_VECTOR_DATABASE" \
  -p 5432:5432 \
  -v "$PGDATA:/var/lib/postgresql" \
  pgvector/pgvector:pg18

docker run -d \
  --name hermes-mem0-ollama \
  --network hermes-mem0 \
  --restart unless-stopped \
  -p 11434:11434 \
  -v "$OLLAMA_DATA:/root/.ollama" \
  ollama/ollama:latest

docker exec hermes-mem0-ollama ollama pull llama3.1:8b
docker exec hermes-mem0-ollama ollama pull nomic-embed-text

For Postgres 18, notice the volume mount: /var/lib/postgresql, not /var/lib/postgresql/data. More on that in the footguns section.

I also create a dedicated database user for Mem0 and enable the vector extension:

DO $$
BEGIN
  IF NOT EXISTS (SELECT FROM pg_catalog.pg_roles WHERE rolname = 'mem0_vector') THEN
    CREATE ROLE mem0_vector LOGIN PASSWORD '<generated-password>';
  ELSE
    ALTER ROLE mem0_vector WITH PASSWORD '<generated-password>';
  END IF;
END
$$;

CREATE EXTENSION IF NOT EXISTS vector;
GRANT ALL PRIVILEGES ON DATABASE mem0 TO mem0_vector;
GRANT ALL ON SCHEMA public TO mem0_vector;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO mem0_vector;

This keeps the app user separate from the Postgres superuser, which is boring and correct.

Hermes Configuration

Hermes needs to know that Mem0 is the active external memory provider:

memory:
  memory_enabled: true
  user_profile_enabled: true
  provider: mem0

Then Mem0's behaviour lives in ~/.hermes/mem0.json.

This is the working shape:

{
  "mode": "oss",
  "user_id": "my-user-id",
  "agent_id": "hermes",
  "oss": {
    "llm": {
      "provider": "ollama",
      "config": {
        "model": "llama3.1:8b",
        "ollama_base_url": "http://localhost:11434",
        "temperature": 0.1,
        "max_tokens": 2000
      }
    },
    "embedder": {
      "provider": "ollama",
      "config": {
        "model": "nomic-embed-text",
        "ollama_base_url": "http://localhost:11434",
        "embedding_dims": 768
      }
    },
    "vector_store": {
      "provider": "pgvector",
      "config": {
        "host": "localhost",
        "port": 5432,
        "user": "mem0_vector",
        "password": "<generated-url-safe-password>",
        "dbname": "mem0",
        "collection_name": "mem0",
        "embedding_model_dims": 768,
        "hnsw": true,
        "diskann": false,
        "sslmode": "disable"
      }
    }
  }
}

The key thing here is that both the LLM and embedder are ollama. That keeps the setup self-hosted. If the embedder falls back to OpenAI, writes will fail unless OPENAI_API_KEY is set, which defeats the point of doing the local setup in the first place.

Environment Variables

I keep service-level values in ~/.hermes/.env, but only use placeholders here:

MEM0_MODE=oss
MEM0_USER_ID=my-user-id
MEM0_AGENT_ID=hermes
MEM0_OLLAMA_MODEL=llama3.1:8b
MEM0_OLLAMA_EMBED_MODEL=nomic-embed-text
MEM0_EMBEDDER=ollama
OLLAMA_BASE_URL=http://localhost:11434

POSTGRES_ROOT_PASSWORD=<generated-url-safe-password>
POSTGRES_VECTOR_USER=mem0_vector
POSTGRES_VECTOR_PASSWORD=<generated-url-safe-password>
POSTGRES_VECTOR_DATABASE=mem0
POSTGRES_VECTOR_HOST=localhost
POSTGRES_VECTOR_PORT=5432
DOCKER_BASE_FOLDER=$HOME/.hermes/docker

One small shell scripting lesson: do not blindly source ~/.hermes/.env from a helper script unless every value is shell-safe. A path with a space in it can break the script. I ended up writing a tiny KEY=value reader for only the variables the script needed.

Restart Hermes

After changing config.yaml, mem0.json, or .env, restart Hermes sessions and any gateway process. The memory provider is loaded into the agent context at session start, so a running gateway may keep old settings in memory.

hermes gateway stop
hermes gateway start

Then check:

hermes memory status

I want to see:

Provider:  mem0
Status:    available

From an agent session, test both reads and writes:

mem0_list
mem0_add content="User prefers local-first infrastructure for agent memory."
mem0_search query="local-first infrastructure"

Reads alone are not enough. A broken embedder can make mem0_list work while writes fail. Writes prove the full path: Ollama extraction, Ollama embeddings, pgvector insert.

How I Use It

My mental split is:

  • Built-in Hermes memory is for short, always-relevant facts.
  • Mem0 is for searchable long-term history and facts that may matter later.

I added a note to the user profile telling Hermes to use both. That matters because agents are literal-minded: if the tool exists but you don't nudge the behaviour, it may not check memory at the right time.

The nice thing about this setup is that the system remains inspectable. I can ask Hermes to search Mem0, list memories, update bad entries, or delete junk. If something breaks, the stack is understandable: Postgres, pgvector, Ollama, Hermes.

Gotchas / Footguns

hermes doctor may complain about MEM0_API_KEY even in OSS mode.
Mem0 Platform needs an API key. Mem0 OSS does not. Trust hermes memory status and the actual mem0.json mode before assuming the doctor output is right.

Postgres 18 changed the Docker data directory expectations.
With pgvector/pgvector:pg18, mount the parent directory at /var/lib/postgresql, not /var/lib/postgresql/data. If you mount the old path, the container can restart forever with a warning about Postgres 18 data layout.

Use URL-safe database passwords.
Mem0's pgvector backend builds a Postgres connection URI internally. A password containing /, @, :, or similar URI-significant characters can be misparsed in bizarre ways. Hex from openssl rand -hex 32 is boring and safe.

Reads can work while writes fail.
mem0_list can succeed even when writes are broken. Writes need the LLM, embedder, vector dimensions, and database permissions to all line up.

The embedder config must be in mem0.json.
Setting MEM0_EMBEDDER=ollama in .env is not enough if ~/.hermes/mem0.json still says the embedder is OpenAI or some other provider.

Restart the gateway after memory changes.
Hermes loads memory provider context at session start. If you edit mem0.json and keep chatting through an old gateway session, you may be testing stale config.

Don't source a messy .env in shell helpers.
Many app .env files contain paths, comments, or values that are fine for dotenv parsers but not safe as shell. Read only the keys you need.

TEI is optional complexity here.
I tried a separate Hugging Face Text Embeddings Inference container first. It is a perfectly valid tool, but for this Hermes + Mem0 setup, Ollama embeddings were simpler and worked with fewer moving parts.

Comments