Mem0
-
Running Mem0's Memory Backend as Real Infrastructure with Ansible
Mem0 works best when you treat its memory backend as infrastructure, not as some throwaway process you start by hand and forget about. There are two pieces worth automating early: the vector store and the graph store. Qdrant holds the embeddings for semantic lookup. Neo4j holds the relationship-oriented graph memory.
Quick note before we go further, because this can be confusing. When folks talk about Mem0’s “graph” support, that is not GraphQL. GraphQL is an API query layer you’d put in front of your app. Mem0’s graph memory is an actual graph database, usually Neo4j, that the client talks to over Bolt. Different thing entirely.
This post walks through a practical Ansible shape for running those backing services with Docker Compose. I’m not going to hand you a complete role. The point is to show the decisions that make the setup repeatable, because those are the parts people usually get wrong.
What the deployment actually does
Three responsibilities, that’s it:
- Get the Mem0 compose project onto the server.
- Write a server-local
.envfile with the runtime config. - Start the Compose stack with Qdrant and Neo4j.
In a small setup, the Python client runs on your laptop while the services run on a LAN host or a small VPS. The server exposes these ports, but only to trusted networks:
- Qdrant HTTP:
6333 - Qdrant gRPC:
6334 - Neo4j browser:
7474 - Neo4j Bolt:
7687
If you’re on a public internet host, bind these to localhost or hide them behind a VPN, firewall, or private network. Do not casually publish database ports to the internet. I shouldn’t have to say remind you…
Role inputs
A generalized role only needs a handful of variables:
mem0_home: /opt/mem0 mem0_repo: [email protected]:your-org/your-mem0-project.git mem0_branch: main mem0_qdrant_collection: default mem0_neo4j_enabled: true mem0_embedder_base_url: https://api.example.com/v1 mem0_embedder_model: your-embedding-model mem0_embedder_dimensions: 1024The collection name matters more than it looks. Qdrant collections have a fixed vector size. If you switch embedding models and the dimensions change, create a new collection. Don’t try to reuse the old one. This is a common way people break a working Mem0 setup, so treat the collection name as part of the embedding config, not an afterthought.
Secrets
Keep API keys and database passwords out of your regular vars files. Ansible Vault, a secrets manager, whatever your deployment system gives you. Then template a
.envfile with tight permissions:- name: Write Mem0 environment file ansible.builtin.template: src: mem0.env.j2 dest: "{{ mem0_home }}/.env" mode: "0600" no_log: trueThat
no_log: trueis not optional. Template diffs will happily leak plaintext API keys and graph passwords into CI logs, terminal scrollback, and ticket attachments. Once a secret lands in a CI log, you’re rotating it, not deleting it.The template itself stays small:
EMBEDDER_API_KEY={{ vault_embedder_api_key }} EMBEDDER_MODEL={{ mem0_embedder_model }} EMBEDDER_DIMENSIONS={{ mem0_embedder_dimensions }} QDRANT_HOST=qdrant QDRANT_PORT=6333 QDRANT_COLLECTION={{ mem0_qdrant_collection }} NEO4J_URL=bolt://neo4j:7687 NEO4J_USERNAME=neo4j NEO4J_PASSWORD={{ vault_neo4j_password }}Notice
QDRANT_HOST=qdrant, not an IP. When the Mem0 container talks to sibling Compose services, use the service names. Save hostnames and LAN DNS for clients that live outside the Compose network. More on that in a second, because it’s a real gotcha.Compose shape
Just what you need to run Mem0’s memory backend in Docker.
services: qdrant: image: qdrant/qdrant:v1.12.6 ports: - "6333:6333" - "6334:6334" volumes: - qdrant_storage:/qdrant/storage neo4j: image: neo4j:5.26 environment: NEO4J_AUTH: "neo4j/${NEO4J_PASSWORD}" ports: - "7474:7474" - "7687:7687" volumes: - neo4j_data:/data - neo4j_logs:/logs volumes: qdrant_storage: neo4j_data: neo4j_logs:Pin your image versions. Floating tags make it impossible to tell whether a later failure came from your playbook, your app, or an upstream image that changed under you at 2am. Pinning turns a mystery into a diff.
The two Ansible flags that matter
Most of the role can stay boring. Ensure the directory exists, clone the repo, template the
.env, start the stack withcommunity.docker.docker_compose_v2. The two details I want you to actually read areforce: falseon the git task andremove_orphans: trueon the compose task.force: falseprotects local edits in the checkout. If the role needs to patch a generated file, make that explicit and safe instead of letting Git clobber the tree.remove_orphans: truekeeps Compose honest. Say you rip out an old local embedding service and switch to a managed embedding API. Without this, the old container just keeps running forever, quietly, and you’ll swear the new config isn’t taking effect.And for restarts, use handlers. Notify a restart handler when the checkout changes or when
.envchanges. Don’t bounce the stack on every single playbook run. The steady-state run should be quiet.Service names vs. hostnames
From a client running outside the Compose network, point Mem0 at the server’s reachable hostname:
QDRANT_HOST=mem0.example.test NEO4J_URL=bolt://mem0.example.test:7687From a container inside the same Compose project, use service names:
QDRANT_HOST=qdrant NEO4J_URL=bolt://neo4j:7687Get this backwards and you burn an afternoon on it…
Check your work
After the playbook runs, verify the pieces independently:
docker compose ps curl http://localhost:6333/healthz docker compose logs --tail=100 neo4jFor Neo4j, actually test Bolt from the network where your client lives. The browser port on
7474being reachable does not prove the Bolt endpoint on7687is usable. Different port, different assumption, don’t confuse a green browser page for a working client.The whole pattern
Strip away the YAML and it’s simple. Docker Compose owns Qdrant and Neo4j. Ansible owns the checkout, the
.env, and the Compose lifecycle. Vault owns the credentials. The client picks service names or external hostnames depending on where it runs.That gets you a repeatable Mem0 backend without turning your Ansible role into a second copy of the entire app. Pin your images, guard your secrets with
no_log, and never reuse a collection after the vector size changes. Do those three and the rest is boring, which is exactly what you want from infrastructure.I’d appreciate a follow. You can subscribe with your email below. The emails go out once a week, or you can find me on Mastodon at @[email protected].