<rss version="2.0">
  <channel>
    <title>Neo4j on LLBBL Blog</title>
    <link>https://llbbl.blog/categories/neo4j/</link>
    <description></description>
    
    <language>en</language>
    
    <lastBuildDate>Thu, 09 Jul 2026 10:00:00 -0500</lastBuildDate>
    
    <item>
      <title>Running Mem0&#39;s Memory Backend as Real Infrastructure with Ansible</title>
      <link>https://llbbl.blog/2026/07/09/running-mems-memory-backend-as.html</link>
      <pubDate>Thu, 09 Jul 2026 10:00:00 -0500</pubDate>
      
      <guid>http://llbbl.micro.blog/2026/07/09/running-mems-memory-backend-as.html</guid>
      <description>&lt;p&gt;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. &lt;strong&gt;Qdrant&lt;/strong&gt; holds the embeddings for semantic lookup. &lt;strong&gt;Neo4j&lt;/strong&gt; holds the relationship-oriented graph memory.&lt;/p&gt;
&lt;p&gt;Quick note before we go further, because this can be confusing. When folks talk about Mem0&amp;rsquo;s &amp;ldquo;graph&amp;rdquo; support, that is &lt;em&gt;not&lt;/em&gt; GraphQL. GraphQL is an API query layer you&amp;rsquo;d put in front of your app. Mem0&amp;rsquo;s graph memory is an actual graph database, usually Neo4j, that the client talks to over Bolt. Different thing entirely.&lt;/p&gt;
&lt;p&gt;This post walks through a practical Ansible shape for running those backing services with Docker Compose. I&amp;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.&lt;/p&gt;
&lt;h2 id=&#34;what-the-deployment-actually-does&#34;&gt;What the deployment actually does&lt;/h2&gt;
&lt;p&gt;Three responsibilities, that&amp;rsquo;s it:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Get the Mem0 compose project onto the server.&lt;/li&gt;
&lt;li&gt;Write a server-local &lt;code&gt;.env&lt;/code&gt; file with the runtime config.&lt;/li&gt;
&lt;li&gt;Start the Compose stack with Qdrant and Neo4j.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;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:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Qdrant HTTP: &lt;code&gt;6333&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Qdrant gRPC: &lt;code&gt;6334&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Neo4j browser: &lt;code&gt;7474&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Neo4j Bolt: &lt;code&gt;7687&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you&amp;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&amp;rsquo;t have to say remind you&amp;hellip;&lt;/p&gt;
&lt;h2 id=&#34;role-inputs&#34;&gt;Role inputs&lt;/h2&gt;
&lt;p&gt;A generalized role only needs a handful of variables:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-yaml&#34; data-lang=&#34;yaml&#34;&gt;&lt;span style=&#34;color:#f92672&#34;&gt;mem0_home&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;/opt/mem0&lt;/span&gt;
&lt;span style=&#34;color:#f92672&#34;&gt;mem0_repo&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;git@github.com:your-org/your-mem0-project.git&lt;/span&gt;
&lt;span style=&#34;color:#f92672&#34;&gt;mem0_branch&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;main&lt;/span&gt;

&lt;span style=&#34;color:#f92672&#34;&gt;mem0_qdrant_collection&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;default&lt;/span&gt;
&lt;span style=&#34;color:#f92672&#34;&gt;mem0_neo4j_enabled&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;true&lt;/span&gt;

&lt;span style=&#34;color:#f92672&#34;&gt;mem0_embedder_base_url&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;https://api.example.com/v1&lt;/span&gt;
&lt;span style=&#34;color:#f92672&#34;&gt;mem0_embedder_model&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;your-embedding-model&lt;/span&gt;
&lt;span style=&#34;color:#f92672&#34;&gt;mem0_embedder_dimensions&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;1024&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The collection name matters more than it looks. Qdrant collections have a &lt;strong&gt;fixed vector size&lt;/strong&gt;. If you switch embedding models and the dimensions change, create a &lt;em&gt;new&lt;/em&gt; collection. Don&amp;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.&lt;/p&gt;
&lt;h2 id=&#34;secrets&#34;&gt;Secrets&lt;/h2&gt;
&lt;p&gt;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 &lt;code&gt;.env&lt;/code&gt; file with tight permissions:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-yaml&#34; data-lang=&#34;yaml&#34;&gt;- &lt;span style=&#34;color:#f92672&#34;&gt;name&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;Write Mem0 environment file&lt;/span&gt;
  &lt;span style=&#34;color:#f92672&#34;&gt;ansible.builtin.template&lt;/span&gt;:
    &lt;span style=&#34;color:#f92672&#34;&gt;src&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;mem0.env.j2&lt;/span&gt;
    &lt;span style=&#34;color:#f92672&#34;&gt;dest&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;{{ mem0_home }}/.env&amp;#34;&lt;/span&gt;
    &lt;span style=&#34;color:#f92672&#34;&gt;mode&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;0600&amp;#34;&lt;/span&gt;
  &lt;span style=&#34;color:#f92672&#34;&gt;no_log&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That &lt;code&gt;no_log: true&lt;/code&gt; 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&amp;rsquo;re rotating it, not deleting it.&lt;/p&gt;
&lt;p&gt;The template itself stays small:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-dotenv&#34; data-lang=&#34;dotenv&#34;&gt;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 }}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Notice &lt;code&gt;QDRANT_HOST=qdrant&lt;/code&gt;, 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 &lt;em&gt;outside&lt;/em&gt; the Compose network. More on that in a second, because it&amp;rsquo;s a real gotcha.&lt;/p&gt;
&lt;h2 id=&#34;compose-shape&#34;&gt;Compose shape&lt;/h2&gt;
&lt;p&gt;Just what you need to run Mem0&amp;rsquo;s memory backend in Docker.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-yaml&#34; data-lang=&#34;yaml&#34;&gt;&lt;span style=&#34;color:#f92672&#34;&gt;services&lt;/span&gt;:
  &lt;span style=&#34;color:#f92672&#34;&gt;qdrant&lt;/span&gt;:
    &lt;span style=&#34;color:#f92672&#34;&gt;image&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;qdrant/qdrant:v1.12.6&lt;/span&gt;
    &lt;span style=&#34;color:#f92672&#34;&gt;ports&lt;/span&gt;:
      - &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;6333:6333&amp;#34;&lt;/span&gt;
      - &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;6334:6334&amp;#34;&lt;/span&gt;
    &lt;span style=&#34;color:#f92672&#34;&gt;volumes&lt;/span&gt;:
      - &lt;span style=&#34;color:#ae81ff&#34;&gt;qdrant_storage:/qdrant/storage&lt;/span&gt;

  &lt;span style=&#34;color:#f92672&#34;&gt;neo4j&lt;/span&gt;:
    &lt;span style=&#34;color:#f92672&#34;&gt;image&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;neo4j:5.26&lt;/span&gt;
    &lt;span style=&#34;color:#f92672&#34;&gt;environment&lt;/span&gt;:
      &lt;span style=&#34;color:#f92672&#34;&gt;NEO4J_AUTH&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;neo4j/${NEO4J_PASSWORD}&amp;#34;&lt;/span&gt;
    &lt;span style=&#34;color:#f92672&#34;&gt;ports&lt;/span&gt;:
      - &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;7474:7474&amp;#34;&lt;/span&gt;
      - &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;7687:7687&amp;#34;&lt;/span&gt;
    &lt;span style=&#34;color:#f92672&#34;&gt;volumes&lt;/span&gt;:
      - &lt;span style=&#34;color:#ae81ff&#34;&gt;neo4j_data:/data&lt;/span&gt;
      - &lt;span style=&#34;color:#ae81ff&#34;&gt;neo4j_logs:/logs&lt;/span&gt;

&lt;span style=&#34;color:#f92672&#34;&gt;volumes&lt;/span&gt;:
  &lt;span style=&#34;color:#f92672&#34;&gt;qdrant_storage&lt;/span&gt;:
  &lt;span style=&#34;color:#f92672&#34;&gt;neo4j_data&lt;/span&gt;:
  &lt;span style=&#34;color:#f92672&#34;&gt;neo4j_logs&lt;/span&gt;:
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;Pin your image versions.&lt;/strong&gt; 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.&lt;/p&gt;
&lt;h2 id=&#34;the-two-ansible-flags-that-matter&#34;&gt;The two Ansible flags that matter&lt;/h2&gt;
&lt;p&gt;Most of the role can stay boring. Ensure the directory exists, clone the repo, template the &lt;code&gt;.env&lt;/code&gt;, start the stack with &lt;code&gt;community.docker.docker_compose_v2&lt;/code&gt;. The two details I want you to actually read are &lt;code&gt;force: false&lt;/code&gt; on the git task and &lt;code&gt;remove_orphans: true&lt;/code&gt; on the compose task.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;force: false&lt;/code&gt; 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.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;remove_orphans: true&lt;/code&gt; 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&amp;rsquo;ll swear the new config isn&amp;rsquo;t taking effect.&lt;/p&gt;
&lt;p&gt;And for restarts, use handlers. Notify a restart handler when the checkout changes or when &lt;code&gt;.env&lt;/code&gt; changes. Don&amp;rsquo;t bounce the stack on every single playbook run. The steady-state run should be quiet.&lt;/p&gt;
&lt;h2 id=&#34;service-names-vs-hostnames&#34;&gt;Service names vs. hostnames&lt;/h2&gt;
&lt;p&gt;From a client running &lt;em&gt;outside&lt;/em&gt; the Compose network, point Mem0 at the server&amp;rsquo;s reachable hostname:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-dotenv&#34; data-lang=&#34;dotenv&#34;&gt;QDRANT_HOST=mem0.example.test
NEO4J_URL=bolt://mem0.example.test:7687
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;From a container &lt;em&gt;inside&lt;/em&gt; the same Compose project, use service names:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-dotenv&#34; data-lang=&#34;dotenv&#34;&gt;QDRANT_HOST=qdrant
NEO4J_URL=bolt://neo4j:7687
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Get this backwards and you burn an afternoon on it&amp;hellip;&lt;/p&gt;
&lt;h2 id=&#34;check-your-work&#34;&gt;Check your work&lt;/h2&gt;
&lt;p&gt;After the playbook runs, verify the pieces independently:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;docker compose ps
curl http://localhost:6333/healthz
docker compose logs --tail&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;100&lt;/span&gt; neo4j
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;For Neo4j, actually test Bolt from the network where your client lives. The browser port on &lt;code&gt;7474&lt;/code&gt; being reachable does not prove the Bolt endpoint on &lt;code&gt;7687&lt;/code&gt; is usable. Different port, different assumption, don&amp;rsquo;t confuse a green browser page for a working client.&lt;/p&gt;
&lt;h2 id=&#34;the-whole-pattern&#34;&gt;The whole pattern&lt;/h2&gt;
&lt;p&gt;Strip away the YAML and it&amp;rsquo;s simple. Docker Compose owns Qdrant and Neo4j. Ansible owns the checkout, the &lt;code&gt;.env&lt;/code&gt;, and the Compose lifecycle. Vault owns the credentials. The client picks service names or external hostnames depending on where it runs.&lt;/p&gt;
&lt;p&gt;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 &lt;code&gt;no_log&lt;/code&gt;, 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.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;I&amp;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 &lt;a href=&#34;https://micro.blog/llbbl?remote_follow=1&#34;&gt;@logan@llbbl.blog&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
</description>
    </item>
    
  </channel>
</rss>