<rss version="2.0">
  <channel>
    <title>Local-first on LLBBL Blog</title>
    <link>https://llbbl.blog/categories/local-first/</link>
    <description></description>
    
    <language>en</language>
    
    <lastBuildDate>Sat, 01 Aug 2026 10:00:00 -0500</lastBuildDate>
    
    <item>
      <title>Run Your Whole Agent Stack on a $5 Box</title>
      <link>https://llbbl.blog/2026/08/01/run-your-whole-agent-stack.html</link>
      <pubDate>Sat, 01 Aug 2026 10:00:00 -0500</pubDate>
      
      <guid>http://llbbl.micro.blog/2026/08/01/run-your-whole-agent-stack.html</guid>
      <description>&lt;p&gt;I SSH&amp;rsquo;d into my home server this afternoon and ran &lt;code&gt;docker stats&lt;/code&gt; on the memory layer that every one of my coding agent sessions talks to. Here&amp;rsquo;s what came back:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;mem0-qdrant    28.09MiB / 60.75GiB    2.13%
mem0-neo4j     612.7MiB / 60.75GiB    0.77%
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;640 megabytes.&lt;/strong&gt; Vector store and graph store, both up for three weeks straight, serving every &lt;code&gt;remember&lt;/code&gt; and &lt;code&gt;recall&lt;/code&gt; call my agents make. The entire persistent memory for my AI tooling uses less RAM than one Chrome tab with Figma open.&lt;/p&gt;
&lt;p&gt;So let&amp;rsquo;s talk about why you&amp;rsquo;re paying a monthly subscription for this.&lt;/p&gt;
&lt;h2 id=&#34;local-first-not-local-only&#34;&gt;Local-First, Not Local-Only&lt;/h2&gt;
&lt;p&gt;I want to be precise here, because &amp;ldquo;self-hosted AI&amp;rdquo; has become a phrase people use to mean nine different things.&lt;/p&gt;
&lt;p&gt;My setup is local-first, not local-only. The &lt;em&gt;state&lt;/em&gt; lives on my hardware. The memories, the embeddings, the graph relationships, everything my agents have learned about my projects, all of it sits on a box I own, in a Docker volume I can &lt;code&gt;tar&lt;/code&gt; and carry away. Nobody can deprecate it, price-hike it, or sunset it.&lt;/p&gt;
&lt;p&gt;The inference does not. My embedder points at Mistral&amp;rsquo;s managed API. I&amp;rsquo;ll get to why, and how to swap it, but I&amp;rsquo;m not going to pretend otherwise in a post about self-hosting.&lt;/p&gt;
&lt;p&gt;That embedder is the only thing that leaves my network, and only when something actually gets embedded, so when writing a memory and searching for one. Listing, deleting, and every graph operation are local with zero API calls.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;State is what you can&amp;rsquo;t get back. Compute is a commodity you rent by the token.&lt;/strong&gt; Losing access to an API means switching providers. Losing two years of accumulated project context means starting over.&lt;/p&gt;
&lt;h2 id=&#34;the-four-pieces&#34;&gt;The Four Pieces&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Qdrant&lt;/strong&gt; is semantic search. When I ask what it remembers about my package manager preferences, Qdrant turns that into a similarity query and hands back the relevant memories. It&amp;rsquo;s Rust, it&amp;rsquo;s fast, and at &lt;strong&gt;28MB resident&lt;/strong&gt; it&amp;rsquo;s essentially free to run. One gotcha: vector dimensions are fixed when the collection is created. Swap embedding models and you need a new collection, not a migration. I learned that the annoying way.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Neo4j&lt;/strong&gt; is the graph store. Vectors are great at &amp;ldquo;find me things that sound like this&amp;rdquo; and bad at &amp;ldquo;what depends on what.&amp;rdquo; The graph holds explicit subject-predicate-object facts, so &lt;code&gt;project X&lt;/code&gt; &lt;code&gt;built_with&lt;/code&gt; &lt;code&gt;Python 3.13&lt;/code&gt; is a traversable edge instead of a fuzzy match. It&amp;rsquo;s the heavy one at &lt;strong&gt;613MB&lt;/strong&gt;, but it&amp;rsquo;s a JVM, so that&amp;rsquo;s mostly heap floor rather than working set. If you&amp;rsquo;re squeezing onto the smallest possible VPS, interrogate this one first.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;mem0&lt;/strong&gt; is the orchestration on top: what gets extracted from a conversation, what gets deduped against existing memories, what gets written where. That&amp;rsquo;s the difference between a database and a memory system.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The MCP server&lt;/strong&gt; is what makes any of it useful. A small Go binary that speaks Model Context Protocol over stdio to Claude Code, exposing eight tools: &lt;code&gt;remember&lt;/code&gt;, &lt;code&gt;recall&lt;/code&gt;, &lt;code&gt;list_memories&lt;/code&gt;, &lt;code&gt;forget&lt;/code&gt;, &lt;code&gt;memory_stats&lt;/code&gt;, &lt;code&gt;add_relation&lt;/code&gt;, &lt;code&gt;recall_related&lt;/code&gt;, &lt;code&gt;forget_relation&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The topology is deliberately boring:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;Mac                                    Home server
┌──────────────┐   ┌──────────┐        ┌─────────────────┐
│ Claude Code  │◄─►│ mem0-mcp │  LAN   │ Qdrant + Neo4j  │
│              │   │ (Go)     │───────►│                 │
└──────────────┘   └──────────┘        └─────────────────┘
       stdio                    HTTP + bolt
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Client binary on my laptop, containers on a box. No cloud in the middle, no account, no dashboard, no seat license.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;the-ansible-role-is-the-whole-argument&#34;&gt;The Ansible Role Is the Whole Argument&lt;/h2&gt;
&lt;p&gt;Anyone can &lt;code&gt;docker compose up&lt;/code&gt; a stack once. That&amp;rsquo;s a weekend, not infrastructure. What makes this real is that it&amp;rsquo;s a role in a repo, and rebuilding it on a fresh box is one command:&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;ansible-playbook -i common_hosts home.yml --tags mem0
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That role does the unglamorous work:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Installs a read-only deploy key scoped to exactly one repo, with an SSH &lt;code&gt;Host&lt;/code&gt; alias so it can&amp;rsquo;t collide with my personal GitHub key&lt;/li&gt;
&lt;li&gt;Clones and updates the source at a pinned branch&lt;/li&gt;
&lt;li&gt;Templates a &lt;code&gt;.env&lt;/code&gt; with secrets pulled from Ansible Vault, &lt;code&gt;no_log: true&lt;/code&gt; so nothing leaks into terminal output on a &lt;code&gt;--diff&lt;/code&gt; run&lt;/li&gt;
&lt;li&gt;Brings up the compose stack with &lt;code&gt;remove_orphans: true&lt;/code&gt;, so when I dropped a service upstream, the stale container went with it instead of lingering forever&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Be careful with your secrets and how you are creating your .env files!&lt;/p&gt;
&lt;h2 id=&#34;the-honest-part-about-the-api-key&#34;&gt;The Honest Part About the API Key&lt;/h2&gt;
&lt;p&gt;I self-host the state and rent the inference. Two reasons.&lt;/p&gt;
&lt;p&gt;The first reason is speed. I ran embeddings locally before this, on CPU, and it was miserable: roughly 87 seconds to embed 32 memories, against about 2 seconds through a hosted API. That is a 45x difference on an operation sitting directly in the path of every &lt;code&gt;remember&lt;/code&gt; and &lt;code&gt;recall&lt;/code&gt;. A good model on a CPU is still a slow model, and this was never a quality problem.&lt;/p&gt;
&lt;p&gt;The second is that embeddings have gotten cheap enough that not worth the time to setup your own embedding service. &lt;a href=&#34;https://mistral.ai/pricing/api&#34;&gt;Mistral&lt;/a&gt; charges &lt;strong&gt;$0.10 per million tokens&lt;/strong&gt; for &lt;code&gt;mistral-embed&lt;/code&gt;. Google&amp;rsquo;s &lt;a href=&#34;https://ai.google.dev/gemini-api/docs/pricing&#34;&gt;&lt;code&gt;gemini-embedding-001&lt;/code&gt;&lt;/a&gt; is &lt;strong&gt;$0.15 per million&lt;/strong&gt;, halved on their batch API. Both are good models. Both bill you.&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://developers.cloudflare.com/workers-ai/platform/pricing/&#34;&gt;Cloudflare&lt;/a&gt; is the worth knowing about if you&amp;rsquo;d rather not pay at all. Workers AI includes &lt;strong&gt;10,000 neurons per day free&lt;/strong&gt;, on the free plan as well as the paid one. Neurons are their normalized compute unit, and &lt;code&gt;bge-m3&lt;/code&gt; costs 1,075 of them per million input tokens — so that daily allowance is roughly &lt;strong&gt;nine million tokens a day, at no cost.&lt;/strong&gt; Past it you&amp;rsquo;re at $0.012 per million, which is an order of magnitude under the paid competition. For a personal memory layer, nine million tokens a day is not a trial. It&amp;rsquo;s just free.&lt;/p&gt;
&lt;p&gt;One detail if you&amp;rsquo;re swapping: &lt;code&gt;bge-m3&lt;/code&gt; emits 1024-dimension vectors, the same as &lt;code&gt;mistral-embed&lt;/code&gt;. Go back to that Qdrant gotcha — matching dimensions means your existing collection still works. Mismatched ones mean starting over.&lt;/p&gt;
&lt;p&gt;And the escape hatch is already built. The env vars in my role are &lt;code&gt;TEI_BASE_URL&lt;/code&gt;, &lt;code&gt;TEI_MODEL&lt;/code&gt;, &lt;code&gt;TEI_DIMENSIONS&lt;/code&gt;. Generic OpenAI-compatible embedder knobs, named after Text Embeddings Inference for historical reasons and pointed at Mistral today. Aim them at a self-hosted TEI container, at Ollama, at anything speaking that shape, and the rest of the stack doesn&amp;rsquo;t notice.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s what local-first buys you. Not purity. &lt;strong&gt;Optionality.&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id=&#34;so-the-5-box&#34;&gt;So, the $5 Box&lt;/h2&gt;
&lt;p&gt;My server has 60GB of RAM, which is absurd overkill and exists because it does a dozen other things. The stack itself measured &lt;strong&gt;640MB with three weeks of uptime&lt;/strong&gt;, essentially zero CPU at idle.&lt;/p&gt;
&lt;p&gt;That fits comfortably on a small cloud VPS in the few-dollars-a-month range. Check current pricing yourself rather than trusting a number in a blog post, but the shape is: a 2 vCPU / 4GB instance from Hetzner or similar costs less per month than one seat of most AI memory SaaS products, and you get to run everything else on it too.&lt;/p&gt;
&lt;p&gt;Your real constraint is RAM, specifically Neo4j&amp;rsquo;s JVM floor. On a 1GB instance you&amp;rsquo;d be fighting it. At 2GB you&amp;rsquo;re fine. At 4GB you&amp;rsquo;ll forget it&amp;rsquo;s running.&lt;/p&gt;
&lt;h2 id=&#34;why-i-care&#34;&gt;Why I Care&lt;/h2&gt;
&lt;p&gt;The indie web ethos is about noticing that renting your identity from a platform means the platform decides what happens to it.&lt;/p&gt;
&lt;p&gt;We&amp;rsquo;re about to make the same mistake with agent memory, except worse, because the thing being accumulated this time is a working model of how you think and what you&amp;rsquo;re building. Every &amp;ldquo;our AI remembers you across sessions&amp;rdquo; product is a proposal that you deposit that into someone else&amp;rsquo;s database and hope the pricing page stays reasonable.&lt;/p&gt;
&lt;p&gt;Qdrant is Apache 2.0. Neo4j Community is GPL. Docker Compose is a YAML file. Ansible is idempotent YAML. Nothing in this stack is exotic. The barrier to owning your agent memory is &lt;strong&gt;an afternoon and 640 megabytes.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Not everyone needs this, and I&amp;rsquo;m not going to pretend a solo dev with three side projects is being exploited by a $20 subscription. But if you&amp;rsquo;re accumulating context you&amp;rsquo;d be genuinely sad to lose, the math changes. Own the state, rent the compute, and keep the role in version control so the whole thing is reproducible on a box you haven&amp;rsquo;t bought yet.&lt;/p&gt;
&lt;p&gt;Moving the embedder onto Cloudflare&amp;rsquo;s free tier is next on my list, what&amp;rsquo;s on yours?&lt;/p&gt;
&lt;hr&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>