Buzz Coding Agents, Part 2: Moving From a VPS to a Home Server

2026-08-20

Part 2: I moved my Buzz coding agents off the VPS and onto a home computer

In part 1 I wrote about self-hosting Buzz, Block's open source chat workspace for humans and coding agents, on a small Hetzner VPS. It worked, but the post ended on an open problem: agents kept hitting an artificial ~1 GB / 1-core ceiling, and the kernel's OOM killer would silently take out a build with no explanation in the chat.

Since then I've moved the coding agents off that VPS entirely. The relay itself still runs there — it's a lightweight chat server, not the bottleneck. The agents now run on a machine I bought specifically for this: 32 GB of RAM, 8 cores, sitting in my home office.

Why a VPS ceiling wasn't just a tuning problem

Some of what I wanted agents to do isn't a "give it more memory" problem, it's a "this VPS categorically cannot do it" problem:

  • Android emulation needs KVM. Booting an Android Virtual Device for real flutter test runs (not just flutter analyze) needs hardware-accelerated virtualization. Most budget VPS plans don't expose nested virtualization, and mine didn't.
  • Headless Chromium under memory pressure is flaky, not just slow. Playwright screenshots and console-error checks would occasionally fail or time out once the container was already close to its memory limit, and it was never obvious whether that was a real bug or resource starvation.
  • Every agent duplicated its own toolchain. Each Docker container carried its own Node, Flutter, and Android SDK install. That's fine for one agent; it adds up fast once you want several, and it made "did the emulator not appear because of a bug, or because this container's SDK image is stale" a recurring question.

None of that is really about cost. It's about being able to say "the agent actually ran the app and watched it work," not "the agent's code compiles and its unit tests didn't touch the DOM."

The new setup: native processes, not containers

The home machine runs Flutter, the Android SDK (with a pre-built AVD), a JDK, and Node under the user's home directory — no sudo, no per-agent container image. Two new agents run as long-lived processes on top of that, alongside the existing Copilot session:

The reasoning for going native instead of one-container-per-agent: three separate Docker images each carrying their own copy of the Android SDK and Flutter would have wasted disk and, worse, made KVM passthrough into every container its own fight. Running natively means every agent shares one real Android emulator, one real Chromium install, one real node_modules cache — and I only had to solve the "why won't the emulator boot" problem once.

Each agent still gets its own identity: a separate Nostr keypair, a separate systemd --user service, and its own env file (mode 600) with credentials. loginctl enable-linger keeps the services running across logout and reboot, the same durability the VPS containers had.

The part that surprised me: allowlisting who an agent listens to

On the VPS, every agent in a channel would respond to any @mention from anyone. That's fine in a channel with just me and my wife. It stops being fine once two more agents share the same box and the same channels — I didn't want agent A's routine status update accidentally waking up agent B, or a typo'd mention from one agent triggering a chain reaction in another.

So the new agents run with BUZZ_ACP_RESPOND_TO=allowlist: each one only reacts to @mentions from me and from its sibling agent, and ignores everyone else in the channel, even if they're mentioned. It's a small config flag, but it's the difference between "three agents that can hold a conversation" and "three agents that occasionally step on each other."

What actually got unlocked

The clearest example so far isn't from Flutter, it's from a Next.js analytics change. An agent (running natively, on this machine) needed to verify a new IntersectionObserver-based tracking event fired exactly once per component mount and didn't break an existing click-through flow.

On the VPS, that verification would have stopped at npm run typecheck and a unit test around the pure logic, because there was no reliable way to boot a real browser and watch it. Here, the agent:

  1. Started the Next.js dev server and a static production build on two different ports.
  2. Launched headless Chromium via Playwright against both.
  3. Confirmed the tracking event fired once, even after retyping the input that would re-render the component.
  4. Walked the full click-through flow and confirmed zero console or page errors.

That's the same "validate for real, not just typecheck" principle behind Android: writing a widget test is necessary but not sufficient, actually booting the emulator and running it is what catches the bug a type checker can't see.

Peer awareness: a partial answer to part 1's biggest complaint

Part 1's main complaint about Buzz was trust and visibility — the app tells you a message was delivered, not what happens after. Running agents natively side-by-side on one box gives a partial workaround that wasn't available on the VPS: each agent's system prompt tells it its sibling's name, pubkey, and model, and gives it permission to check on it directly.

Buzz's own presence signal only reflects websocket connectivity — an agent can look "online" while its underlying model call is actually stuck. So the agents also check each other with systemctl --user status buzz-<other>.service and journalctl --user -u buzz-<other>, which is a real liveness signal instead of a websocket ping. It's not the in-app chain-of-thought visibility I still want from Buzz itself, but two agents that can say "yes, my sibling's process is alive and its last log line was five seconds ago" is a meaningfully better answer than silence.

What I gave up

To be fair about the trade-off, since it's the same one every "containers vs. bare metal" decision comes down to:

  • No more per-agent container isolation. All three agents share one filesystem and one set of credentials-adjacent processes. A bug or a compromised dependency in one agent's toolchain is a bigger blast radius than it was in separate containers.
  • A physical box in my house instead of a managed VPS. No provider handling power, networking, or hardware failure for me anymore.
  • Fewer built-in guardrails. The Docker memory limits that caused the original OOM problem were also, accidentally, a safety net. Nothing stops a native build from using all 32 GB now except the agents' own instructions to prefer targeted tests over full production builds unless asked.

I think it's the right trade for what I actually needed — real emulator and browser verification — but it's not free.

Still on the wish list

Same headline item as part 1: Buzz still doesn't show a chain of thought or a "this call has been running for six minutes" signal in the app itself. The systemctl/journalctl peer-check is a workaround specific to running agents on the same host; it wouldn't help at all if the agents were back in separate containers or on separate machines.

A smaller, more mundane one: the Android emulator on this host needs LD_LIBRARY_PATH pointed at its own bundled Qt libraries, or it segfaults on launch with a missing libX11-xcb.so.1. Not a Buzz problem, just Android tooling being Android tooling.

If you're setting up something similar, start with part 1 for the relay/pairing-relay/Caddy setup — none of that changed. The only thing that moved is where the agents themselves run.