{
  "version": "https://jsonfeed.org/version/1",
  "title": "Mem0 on LLBBL Blog",
  "icon": "https://avatars.micro.blog/avatars/2023/40/125738.jpg",
  "home_page_url": "https://llbbl.blog/",
  "feed_url": "https://llbbl.blog/feed.json",
  "items": [
      {
        "id": "http://llbbl.micro.blog/2026/07/09/running-mems-memory-backend-as.html",
        "title": "Running Mem0's Memory Backend as Real Infrastructure with Ansible",
        "content_html": "<p>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. <strong>Qdrant</strong> holds the embeddings for semantic lookup. <strong>Neo4j</strong> holds the relationship-oriented graph memory.</p>\n<p>Quick note before we go further, because this can be confusing. When folks talk about Mem0&rsquo;s &ldquo;graph&rdquo; support, that is <em>not</em> GraphQL. GraphQL is an API query layer you&rsquo;d put in front of your app. Mem0&rsquo;s graph memory is an actual graph database, usually Neo4j, that the client talks to over Bolt. Different thing entirely.</p>\n<p>This post walks through a practical Ansible shape for running those backing services with Docker Compose. I&rsquo;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.</p>\n<h2 id=\"what-the-deployment-actually-does\">What the deployment actually does</h2>\n<p>Three responsibilities, that&rsquo;s it:</p>\n<ol>\n<li>Get the Mem0 compose project onto the server.</li>\n<li>Write a server-local <code>.env</code> file with the runtime config.</li>\n<li>Start the Compose stack with Qdrant and Neo4j.</li>\n</ol>\n<p>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:</p>\n<ul>\n<li>Qdrant HTTP: <code>6333</code></li>\n<li>Qdrant gRPC: <code>6334</code></li>\n<li>Neo4j browser: <code>7474</code></li>\n<li>Neo4j Bolt: <code>7687</code></li>\n</ul>\n<p>If you&rsquo;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&rsquo;t have to say remind you&hellip;</p>\n<h2 id=\"role-inputs\">Role inputs</h2>\n<p>A generalized role only needs a handful of variables:</p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4\"><code class=\"language-yaml\" data-lang=\"yaml\"><span style=\"color:#f92672\">mem0_home</span>: <span style=\"color:#ae81ff\">/opt/mem0</span>\n<span style=\"color:#f92672\">mem0_repo</span>: <span style=\"color:#ae81ff\">git@github.com:your-org/your-mem0-project.git</span>\n<span style=\"color:#f92672\">mem0_branch</span>: <span style=\"color:#ae81ff\">main</span>\n\n<span style=\"color:#f92672\">mem0_qdrant_collection</span>: <span style=\"color:#ae81ff\">default</span>\n<span style=\"color:#f92672\">mem0_neo4j_enabled</span>: <span style=\"color:#66d9ef\">true</span>\n\n<span style=\"color:#f92672\">mem0_embedder_base_url</span>: <span style=\"color:#ae81ff\">https://api.example.com/v1</span>\n<span style=\"color:#f92672\">mem0_embedder_model</span>: <span style=\"color:#ae81ff\">your-embedding-model</span>\n<span style=\"color:#f92672\">mem0_embedder_dimensions</span>: <span style=\"color:#ae81ff\">1024</span>\n</code></pre></div><p>The collection name matters more than it looks. Qdrant collections have a <strong>fixed vector size</strong>. If you switch embedding models and the dimensions change, create a <em>new</em> collection. Don&rsquo;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.</p>\n<h2 id=\"secrets\">Secrets</h2>\n<p>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 <code>.env</code> file with tight permissions:</p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4\"><code class=\"language-yaml\" data-lang=\"yaml\">- <span style=\"color:#f92672\">name</span>: <span style=\"color:#ae81ff\">Write Mem0 environment file</span>\n  <span style=\"color:#f92672\">ansible.builtin.template</span>:\n    <span style=\"color:#f92672\">src</span>: <span style=\"color:#ae81ff\">mem0.env.j2</span>\n    <span style=\"color:#f92672\">dest</span>: <span style=\"color:#e6db74\">&#34;{{ mem0_home }}/.env&#34;</span>\n    <span style=\"color:#f92672\">mode</span>: <span style=\"color:#e6db74\">&#34;0600&#34;</span>\n  <span style=\"color:#f92672\">no_log</span>: <span style=\"color:#66d9ef\">true</span>\n</code></pre></div><p>That <code>no_log: true</code> is 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&rsquo;re rotating it, not deleting it.</p>\n<p>The template itself stays small:</p>\n<pre tabindex=\"0\"><code class=\"language-dotenv\" data-lang=\"dotenv\">EMBEDDER_API_KEY={{ vault_embedder_api_key }}\nEMBEDDER_MODEL={{ mem0_embedder_model }}\nEMBEDDER_DIMENSIONS={{ mem0_embedder_dimensions }}\n\nQDRANT_HOST=qdrant\nQDRANT_PORT=6333\nQDRANT_COLLECTION={{ mem0_qdrant_collection }}\n\nNEO4J_URL=bolt://neo4j:7687\nNEO4J_USERNAME=neo4j\nNEO4J_PASSWORD={{ vault_neo4j_password }}\n</code></pre><p>Notice <code>QDRANT_HOST=qdrant</code>, 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 <em>outside</em> the Compose network. More on that in a second, because it&rsquo;s a real gotcha.</p>\n<h2 id=\"compose-shape\">Compose shape</h2>\n<p>Just what you need to run Mem0&rsquo;s memory backend in Docker.</p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4\"><code class=\"language-yaml\" data-lang=\"yaml\"><span style=\"color:#f92672\">services</span>:\n  <span style=\"color:#f92672\">qdrant</span>:\n    <span style=\"color:#f92672\">image</span>: <span style=\"color:#ae81ff\">qdrant/qdrant:v1.12.6</span>\n    <span style=\"color:#f92672\">ports</span>:\n      - <span style=\"color:#e6db74\">&#34;6333:6333&#34;</span>\n      - <span style=\"color:#e6db74\">&#34;6334:6334&#34;</span>\n    <span style=\"color:#f92672\">volumes</span>:\n      - <span style=\"color:#ae81ff\">qdrant_storage:/qdrant/storage</span>\n\n  <span style=\"color:#f92672\">neo4j</span>:\n    <span style=\"color:#f92672\">image</span>: <span style=\"color:#ae81ff\">neo4j:5.26</span>\n    <span style=\"color:#f92672\">environment</span>:\n      <span style=\"color:#f92672\">NEO4J_AUTH</span>: <span style=\"color:#e6db74\">&#34;neo4j/${NEO4J_PASSWORD}&#34;</span>\n    <span style=\"color:#f92672\">ports</span>:\n      - <span style=\"color:#e6db74\">&#34;7474:7474&#34;</span>\n      - <span style=\"color:#e6db74\">&#34;7687:7687&#34;</span>\n    <span style=\"color:#f92672\">volumes</span>:\n      - <span style=\"color:#ae81ff\">neo4j_data:/data</span>\n      - <span style=\"color:#ae81ff\">neo4j_logs:/logs</span>\n\n<span style=\"color:#f92672\">volumes</span>:\n  <span style=\"color:#f92672\">qdrant_storage</span>:\n  <span style=\"color:#f92672\">neo4j_data</span>:\n  <span style=\"color:#f92672\">neo4j_logs</span>:\n</code></pre></div><p><strong>Pin your image versions.</strong> 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.</p>\n<h2 id=\"the-two-ansible-flags-that-matter\">The two Ansible flags that matter</h2>\n<p>Most of the role can stay boring. Ensure the directory exists, clone the repo, template the <code>.env</code>, start the stack with <code>community.docker.docker_compose_v2</code>. The two details I want you to actually read are <code>force: false</code> on the git task and <code>remove_orphans: true</code> on the compose task.</p>\n<p><code>force: false</code> protects 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.</p>\n<p><code>remove_orphans: true</code> keeps 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&rsquo;ll swear the new config isn&rsquo;t taking effect.</p>\n<p>And for restarts, use handlers. Notify a restart handler when the checkout changes or when <code>.env</code> changes. Don&rsquo;t bounce the stack on every single playbook run. The steady-state run should be quiet.</p>\n<h2 id=\"service-names-vs-hostnames\">Service names vs. hostnames</h2>\n<p>From a client running <em>outside</em> the Compose network, point Mem0 at the server&rsquo;s reachable hostname:</p>\n<pre tabindex=\"0\"><code class=\"language-dotenv\" data-lang=\"dotenv\">QDRANT_HOST=mem0.example.test\nNEO4J_URL=bolt://mem0.example.test:7687\n</code></pre><p>From a container <em>inside</em> the same Compose project, use service names:</p>\n<pre tabindex=\"0\"><code class=\"language-dotenv\" data-lang=\"dotenv\">QDRANT_HOST=qdrant\nNEO4J_URL=bolt://neo4j:7687\n</code></pre><p>Get this backwards and you burn an afternoon on it&hellip;</p>\n<h2 id=\"check-your-work\">Check your work</h2>\n<p>After the playbook runs, verify the pieces independently:</p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4\"><code class=\"language-bash\" data-lang=\"bash\">docker compose ps\ncurl http://localhost:6333/healthz\ndocker compose logs --tail<span style=\"color:#f92672\">=</span><span style=\"color:#ae81ff\">100</span> neo4j\n</code></pre></div><p>For Neo4j, actually test Bolt from the network where your client lives. The browser port on <code>7474</code> being reachable does not prove the Bolt endpoint on <code>7687</code> is usable. Different port, different assumption, don&rsquo;t confuse a green browser page for a working client.</p>\n<h2 id=\"the-whole-pattern\">The whole pattern</h2>\n<p>Strip away the YAML and it&rsquo;s simple. Docker Compose owns Qdrant and Neo4j. Ansible owns the checkout, the <code>.env</code>, and the Compose lifecycle. Vault owns the credentials. The client picks service names or external hostnames depending on where it runs.</p>\n<p>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 <code>no_log</code>, 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.</p>\n<blockquote>\n<p>I&rsquo;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 <a href=\"https://micro.blog/llbbl?remote_follow=1\">@logan@llbbl.blog</a>.</p>\n</blockquote>\n",
        "date_published": "2026-07-09T10:00:00-05:00",
        "url": "https://llbbl.blog/2026/07/09/running-mems-memory-backend-as.html",
        "tags": ["DevOps","docker","Ansible","Mem0","Qdrant","Neo4j"]
      }
  ]
}
