1(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[88152],{19899:function(e,t,n){(window.__NEXT_P=window.__NEXT_P||[]).push(["/blog/buzz-agents-part-2-native-home-server",function(){return n(11881)}])},11881:function(e,t,n){"use strict";n.r(t),n.d(t,{__N_SSG:function(){return i},default:function(){return c},metadata:function(){return d}});var s=n(85893),a=n(91644),r=n(47185),o=n(86983),i=!0;let d={slug:"buzz-agents-part-2-native-home-server",date:"2026-08-20",ogImage:"/og-images/buzz-agents-part-2-native-home-server.jpg",metaTitle:"Buzz Coding Agents, Part 2: Adding Agents on a Homelab",description:"I added three Buzz coding agents on a 32GB homelab and kept the VPS ones for quick asks and orchestration. Why a memory-capped VPS container couldn't run a real Android emulator or headless Chromium, and why the homelab agents run as native systemd services instead of containers.",keywords:"buzz block, coding agents, acp agent client protocol, systemd user service, android emulator kvm, playwright chromium, self-hosted agents, homelab, vps vs bare metal, devtools daily",tags:["infrastructure","devops","ai agents","android","playwright"]},l=function(e){let{children:t}=e;return(0,s.jsx)(r.Z,{title:d.metaTitle,date:d.date,children:t})};function h(e){let t={a:"a",code:"code",h2:"h2",hr:"hr",li:"li",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,a.a)(),...e.components};return(0,s.jsxs)(s.Fragment,{children:[(0,s.jsx)(t.h2,{children:"Part 2: adding Buzz coding agents on a homelab"}),"\n",(0,s.jsx)(t.h2,{children:"Introduction"}),"\n",(0,s.jsxs)(t.p,{children:["In ",(0,s.jsx)(t.a,{href:"/blog/a-week-with-buzz-coding-agents/",children:"part 1"})," I wrote about self-hosting ",(0,s.jsx)(t.a,{href:"https://github.com/block/buzz",children:"Buzz"}),", Block's open source chat workspace for humans and coding agents, on a small Hetzner VPS."]}),"\n",(0,s.jsx)(t.p,{children:"That post ended on an open problem. Each agent container had about 1 GB of memory and one core, and when a build went over that, the kernel's OOM killer took it out silently. In the chat the agent still looked like it was thinking."}),"\n",(0,s.jsx)(t.p,{children:"I didn't move off the VPS. I added three more agents on a homelab machine and kept the VPS ones, so the pool is bigger and split by job. The homelab agents do the heavy work: long-running tasks, real builds, emulator runs, browser tests. The VPS agents stay for quick asks and orchestration."}),"\n",(0,s.jsx)(t.p,{children:"The relay itself never moved. It's a chat server, and it was never the bottleneck."}),"\n",(0,s.jsx)(t.h2,{children:"Why more memory on the VPS wouldn't have fixed it"}),"\n",(0,s.jsx)(t.p,{children:"Some of what I wanted agents to do wasn't a tuning problem."}),"\n",(0,s.jsxs)(t.ul,{children:["\n",(0,s.jsxs)(t.li,{children:[(0,s.jsx)(t.strong,{children:"Android emulation needs KVM."})," Booting an Android Virtual Device for real ",(0,s.jsx)(t.code,{children:"flutter test"})," runs, not just ",(0,s.jsx)(t.code,{children:"flutter analyze"}),", needs hardware virtualization. Most budget VPS plans don't expose nested virtualization, and mine didn't."]}),"\n",(0,s.jsxs)(t.li,{children:[(0,s.jsx)(t.strong,{children:"Headless Chromium gets flaky under memory pressure."})," Playwright screenshots and console-error checks would sometimes fail or time out once the container was near its limit. I could never tell if I was looking at a real bug or at resource starvation."]}),"\n",(0,s.jsxs)(t.li,{children:[(0,s.jsx)(t.strong,{children:"Every agent carried its own toolchain."})," Each Docker container had its own Node, Flutter, and Android SDK. That's fine for one agent. It adds up fast at three, and a stale SDK image became one more thing to rule out when something didn't work."]}),"\n",(0,s.jsxs)(t.li,{children:[(0,s.jsx)(t.strong,{children:"I want proof, not a summary."})," I want every task verified with a real build, plus captured screenshots and video, published to an internal site I can review later. That doesn't happen in a 1 GB container."]}),"\n"]}),"\n",(0,s.jsx)(t.h2,{children:"The homelab box"}),"\n",(0,s.jsx)(t.p,{children:"The homelab is a machine with 32 GB of memory and 8 cores running Ubuntu Server. No desktop, I administer it over SSH. It sits on my home network, and I reach it from outside over Tailscale instead of forwarding ports on the router."}),"\n",(0,s.jsx)(t.h2,{children:"Native processes instead of containers"}),"\n",(0,s.jsxs)(t.p,{children:["The homelab runs Flutter, the Android SDK with a pre-built AVD, a JDK, and Node under ",(0,s.jsx)(t.code,{children:"~/sdk"}),", with no ",(0,s.jsx)(t.code,{children:"sudo"}
1)," and no per-agent image. Three agents run on top of that as long-lived processes, while the VPS agents keep running in their containers:"]}),"\n",(0,s.jsx)(o.Z,{markup:'\nflowchart TB\n subgraph vps["Hetzner VPS (unchanged)"]\n caddy["Caddy \xb7 HTTPS \xb7 wss://buzz.example.com"]\n br["Buzz relay + Postgres + Redis + MinIO"]\n v1["Quick-ask / orchestration agents<br/>(Docker, ~1GB each)"]\n caddy --> br\n v1 --> br\n end\n subgraph home["Homelab \xb7 32GB / 8 cores \xb7 Ubuntu Server"]\n subgraph shared["Shared toolchains (installed once, in ~/sdk)"]\n node["Node.js"]\n flutter["Flutter SDK"]\n android["Android SDK + KVM emulator"]\n jdk["JDK 17"]\n chromium["Playwright Chromium"]\n end\n a1["Agent: Copilot CLI<br/>(systemd user service)"]\n a2["Agent: opus-4.8<br/>(systemd user service)"]\n a3["Agent: gpt-5.6-terra<br/>(systemd user service)"]\n a1 --- shared\n a2 --- shared\n a3 --- shared\n end\n home -->|wss| caddy\n'}),"\n",(0,s.jsxs)(t.p,{children:["I went native instead of one container per agent for a boring reason. Three images would each carry their own copy of the Android SDK and Flutter, and I'd have to get KVM into every one of them. Running natively means one emulator, one Chromium install, one ",(0,s.jsx)(t.code,{children:"node_modules"})," cache, and I only had to figure out the emulator once."]}),"\n",(0,s.jsxs)(t.p,{children:["Each agent still gets its own identity: its own Nostr keypair, its own ",(0,s.jsx)(t.code,{children:"systemd --user"})," service, and its own env file at mode ",(0,s.jsx)(t.code,{children:"600"}),"."]}),"\n",(0,s.jsx)(t.pre,{children:(0,s.jsx)(t.code,{className:"hljs language-bash",children:"systemctl --user status buzz-opus48 buzz-terra buzz-bob\nloginctl enable-linger roman\n"})}),"\n",(0,s.jsxs)(t.p,{children:[(0,s.jsx)(t.code,{children:"enable-linger"})," is the part I'd forget. Without it the user services stop when you log out, and the agents quietly disappear from the channel after a reboot."]}),"\n",(0,s.jsx)(t.h2,{children:"How they work with each other"}),"\n",(0,s.jsx)(t.p,{children:"They talk to each other in Buzz, same as they talk to me. Mostly that means one agent reviews another one's work, which is the thing I'd otherwise have to do myself on every change."}),"\n",(0,s.jsx)(t.p,{children:"The VPS agents didn't retire, they just changed jobs. They handle VPS-specific work and orchestration. A typical flow is me asking a VPS agent to go through open tickets, and it hands the real work to the heavy agents on the homelab."}),"\n",(0,s.jsx)(t.h2,{children:"What I still need to figure out"}),"\n",(0,s.jsx)(t.p,{children:"The setup runs, but four things are still open."}),"\n",(0,s.jsxs)(t.p,{children:[(0,s.jsx)(t.strong,{children:"Distributing work."})," Right now I assign tasks by typing mentions into Buzz. That doesn't scale past a handful of tasks, and nothing tracks what's assigned to whom. ",(0,s.jsx)(t.a,{href:"https://docs.gastownhall.ai/",children:"Gastown"})," looks like the closest thing to what I want: work items with dependencies, short-lived worker agents each in their own git worktree, a reviewer agent, and all the state kept in the repo so it survives a restart. I haven't decided whether to adopt it or write something smaller myself."]}),"\n",(0,s.jsxs)(t.p,{children:[(0,s.jsx)(t.strong,{children:"Screenshots don't look right on Linux."})," Rendering on the homelab isn't as nice as on the Mac. Fonts are the obvious difference, and the captures I get back aren't something I'd want to publish. I either need to pin a font stack on the homelab and accept it looks different, or send the screenshot and video jobs back to the Mac agent and leave builds and tests on the homelab."]}),"\n",(0,s.jsxs)(t.p,{children:[(0,s.jsx)(t.strong,{children:"Seeing what an agent is actually doing."})," This was part 1's complaint and it's still true. I can check that a service is alive, but I can't see the current state of an agent or the sessions it spun up. ",(0,s.jsx)(t.code,{children:"systemctl"})," tells me the process exists, not what it's working on."]}),"\n",(0,s.jsxs)(t.p,{children:[(0,s.jsx)(t.strong,{children:"Whether Buzz is still the right place for this."})," For talking to agents from my phone, it's good, and I'm not replacing it. But I think I've been using a chat app as a work tracker, and that's where most of these problems come from. Chat and work state probably want to be two different things."]}),"\n",(0,s.jsx)(t.h2,{children:"What's next"}),"\n",(0,s.jsx)(t.p,{children:"In the next part I'll write about how I manage work for the agent army, without typing everything into Buzz by hand."}),"\n",(0,s.jsx)(t.hr,{})]})}function c(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};return(0,s.jsx)(l,{...e,children:(0,s.jsx)(h,{...e})})}},47185:function(e,t,n){"use strict";n.d(t,{Z:function(){return l}});var s=n(85893),a=n(43078),r=n(9008),o=n.n(r),i=n(11163);n(48097);let d="https://www.devtoolsdaily.com";function l(e){let{date:t,children:n,title:r,useContainer:l=!0,articleSchema:h}=e,c=(0,i.useRouter)(),u=((null==c?void 0:c.asPath)||"").split("?")[0].split("#")[0],g="".concat(d).concat(u),m=h?JSON.stringify({"@context":"https://schema.org","@type":h.type||"TechArticle",headline:h.title,...h.description?{description:h.description}:{},...h.keywords?{keywords:h.keywords}:{},...h.date?{datePublished:h.date,dateModified:h.date}:{},mainEntityOfPage:{"@type":"WebPage","@id":g},author:{"@type":"Organization",name:"DevTools Daily",url:d},publisher:{"@type":"Organization",name:"DevTools Daily",url:d}}).replace(/</g,"\\u003c"):null,p=u.startsWith("/blog/")&&"/blog/"!==u&&"/blog"!==u?JSON.stringify({"@context":"https://schema.org","@type":"BreadcrumbList",itemListElement:[{"@type":"ListItem",position:1,name:"DevTools Daily",item:"".concat(d,"/")},{"@type":"ListItem",position:2,name:"Blog",item:"".concat(d,"/blog/")},{"@type":"ListItem",position:3,name:r,item:g}]}).replace(/</g,"\\u003c"):null,f=l?(0,s.jsxs)(a.W,{className:"blog-article",children:[(0,s.jsx)("h1",{children:r}),(0,s.jsx)("div",{className:"text-sm italic date pb-1 pt-1",children:t}),n]}):(0,s.jsxs)("div",{className:"blog-article",children:[(0,s.jsx)("h1",{children:r}),n]});return(0,s.jsxs)(s.Fragment,{children:[(m||p)&&(0,s.jsxs)(o(),{children:[m&&(0,s.jsx)("script",{type:"application/ld+json",dangerouslySetInnerHTML:{__html:m}}),p&&(0,s.jsx)("script",{type:"application/ld+json",dangerouslySetInnerHTML:{__html:p}})]}),f]})}},86983:function(e,t,n){"use strict";n.d(t,{Z:function(){return i}});var s=n(85893),a=n(67294),r=n(98539),o=n(46455);function i(e){let{markup:t}=e,n=(0,o.l)("light",{getInitialValueInEffect:!0}),i=(0,a.useRef)(null),[d,l]=(0,a.useState)(void 0),h=(0,a.useId)().replace(/[^a-zA-Z0-9]/g,"");return((0,a.useEffect)(()=>{let e=!1;
1return r.N.initialize({startOnLoad:!1,theme:"dark"===n?"dark":"default",securityLevel:"loose"}),(async()=>{try{let{svg:n}=await r.N.render("sd-mermaid-".concat(h),t);if(e)return;i.current&&(i.current.innerHTML=n),l(void 0)}catch(t){e||l((null==t?void 0:t.message)||"Failed to render diagram")}})(),()=>{e=!0}},[t,n,h]),d)?(0,s.jsx)("pre",{className:"overflow-auto rounded-md border border-red-400 p-3 text-sm text-red-500",children:t}):(0,s.jsx)("div",{ref:i,className:"flex w-full justify-center [&_svg]:max-w-full"})}},91644:function(e,t,n){"use strict";function s(e){return{...e}}n.d(t,{a:function(){return s}})},48097:function(){},43078:function(e,t,n){"use strict";n.d(t,{W:function(){return g}});var s=n(85893);n(67294);var a=n(69429),r=n(13637),o=n(90987),i=n(46976),d=n(33583),l=n(9535),h={root:"m_7485cace"};let c={},u=(0,r.Z)((e,{size:t,fluid:n})=>({root:{"--container-size":n?void 0:(0,a.ap)(t,"container-size")}})),g=(0,l.d5)((e,t)=>{let n=(0,o.w)("Container",c,e),{classNames:a,className:r,style:l,styles:g,unstyled:m,vars:p,fluid:f,mod:w,...b}=n,y=(0,i.y)({name:"Container",classes:h,props:n,className:r,style:l,classNames:a,styles:g,unstyled:m,vars:p,varsResolver:u});return(0,s.jsx)(d.x,{ref:t,mod:[{fluid:f},w],...y("root"),...b})});g.classes=h,g.displayName="@mantine/core/Container"}},function(e){e.O(0,[70152,31748,51039,59231,32702,92888,49774,40179],function(){return e(e.s=19899)}),_N_E=e.O()}]);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.