vibescoder

Modding an Old Chromebox to Run in the Homelab Stack: Why DNS Is the Right Use Case

·16 min read

I had an old Acer Chromebox CXI3, a functioning ThinkCentre, and the familiar homelab urge to give every spare computer a job. The Chromebox had an i7 sticker. The ThinkCentre had an i5. Naturally, my first question was whether I’d been using the wrong machine for the important stuff.

That question turned into a better one. What would improve the homelab because it ran on a different physical box? The answer was backups and DNS. Getting there meant removing a write-protect screw, establishing durable agent access, installing two services, testing a restore, and discovering that moving DNS can break something that never looked related to DNS. The local AI router supplied that last lesson.

Gemini helped with the firmware conversion. The remote configuration session ran in Coder Agents with DeepSeek V4 Flash on my Spark cluster. This writeup comes from those transcripts, drafted here in ChatGPT. The interesting part is the infrastructure I ended up with and the decisions that kept an old computer’s new job appropriately small.

The Ubuntu Install Started With a Screwdriver

The Chromebox still ran ChromeOS. I’d already enabled developer mode when I asked Gemini how to get Ubuntu onto it. The route was MrChromebox’s Full UEFI firmware, followed by a normal Ubuntu USB installer. The broad outline was simple. The details required a little more patience.

Ctrl + Alt + T wasn’t getting me anywhere. Ctrl + Alt + F2 got me to the console. Then the download command appeared to do nothing. Rather than keep pasting the same combined command, I asked to break it into separate steps. Checking whether the downloaded file existed exposed the failure.

The useful error was:

curl: (23) Failure writing output to destination
Read-only file system

Changing to a writable directory, /tmp, got the script onto the machine. Gemini had first guessed a certificate problem. The filesystem error was the evidence that moved the work forward.

The next obstacle was physical. The utility warned that protected firmware ranges would prevent writes. I powered down, opened the case, and removed the write-protect screw. Back in the utility, I could proceed to the Full UEFI firmware option and its stock-firmware backup prompts, then continue to Ubuntu installation. By the next session, Ubuntu was installed and reachable.

The sequence was developer mode, a working console, a writable download location, hardware write-protect removal, firmware replacement, then the OS installer. Developer mode alone hadn’t unlocked the firmware. The screwdriver was part of the software installation.

For anyone following along, use the current MrChromebox instructions for your board. They specify the console, normal-user invocation, and device-dependent menu. This is my conversion story, not a universal instruction to remove an arbitrary screw from a Chromebox.

The i7 Sticker Didn’t Give It the Compute Job

I started the next session by asking the agent to inspect the ThinkCentre. Then I compared it with the Chromebox. The comparison I used to scope their roles looked like this:

HardwareThinkCentreAcer Chromebox CXI3
ProcessorIntel Core i5-8400TIntel Core i7-8650U
Cores / threads6 / 64 / 8
Processor power class35W TDP15W TDP
Memory16 GB DDR416 GB DDR4
Storage256 GB NVMe SSD128 GB NVMe SSD
Existing roleProxmox host for Home Assistant and supporting servicesNewly converted Ubuntu machine

Those TDP figures aren’t measurements of either machine’s idle power consumption. Just spec sheet fodder. More importantly, I needed to understand the difference between the i5 (a desktop-class processor) and the i7 (laptop-class). No surprise, the i5 was significantly more powerful. The core count confirmed it.

The ThinkCentre had headroom. There was no capacity problem that required moving compute onto a smaller mobile-processor box. I considered Whisper and Piper, but decided to leave the speech-layer idea with the ThinkCentre. I also left Frigate out of scope. This wasn’t a benchmark proving those workloads impossible on the Chromebox. It was a decision not to create another tuning project to relieve a machine that wasn’t busy.

A second Home Assistant instance sounded useful until I pointed out that the Zigbee and Z-Wave coordinators were physically attached to the ThinkCentre. Restoring the software elsewhere wouldn’t move the radios. A spare instance could have staging value, but it wouldn’t provide automatic device failover.

That narrowed the job to Proxmox Backup Server and AdGuard Home. Both benefit from physical separation. Neither required me to pretend the Chromebox was the homelab’s new compute champion.

Durable SSH Access Made the Backup Node Possible

Before installing services, I wanted a plan that would survive the chat. The agent wrote it into the content repo’s experiment documents and linked it from the TODO list. That mattered because the plan changed. Initially it included Proxmox VE. The finished version didn’t.

Remote access came first, following the pattern I’d already established for the ThinkCentre in Smart Home, Dumb Luck, Episode 2. A dedicated SSH key became a Coder User Secret with a file target in the workspace. A one-time console bootstrap installed the public key on the Chromebox. An SSH alias made subsequent access consistent.

There are two halves to that arrangement. The target must trust the public key. The agent’s workspace must receive the corresponding private key through the secret mechanism. Creating a secret doesn’t install trust on a machine the agent can’t yet reach. I did that first local step, then the agent checked non-interactive access.

The operational check was ordinary:

ssh -o BatchMode=yes chromebox hostname

It needed to work without stopping for a password. From then on, the session could administer the machine remotely, and future workspaces had a documented access pattern. The versioned homelab skill carried the machine’s role and access reference; the dashboard’s Personal skill copy needed updating too. Having two copies is useful only if I remember that they don’t synchronize themselves.

Then I asked the question that removed an entire layer of infrastructure. Could the Chromebox run PBS and AdGuard without installing Proxmox VE?

Yes. Proxmox VE is the virtualization platform already running on the ThinkCentre. Proxmox Backup Server is the backup service. Receiving backups doesn’t require the receiving box to host another collection of VMs and containers. AdGuard Home can run as a systemd service as well. I wasn’t going to use the extra virtualization layer, so I dropped it.

That also simplified remote administration. I already had Ubuntu installed and console fallback available. Keeping that installation avoided a second disk installation and an unnecessary network/boot transition in the middle of an SSH session.

The architecture became:

ThinkCentre / Proxmox VE
  local-agent-router
  Home Assistant OS VM
  Uptime Kuma container
          |
          | scheduled backups over the LAN
          v
Chromebox / Ubuntu
  Proxmox Backup Server -> directory datastore
  AdGuard Home          -> DNS for configured clients

There was an important packaging wrinkle. The agent initially tried a PBS repository using Ubuntu’s noble codename. It didn’t have a Release file. The session then used Debian bookworm PBS packages, simulated the installation to check dependency resolution, and installed them on Ubuntu 24.04.

That worked in this session. It is a custom installation, not evidence that PBS Server officially supports Ubuntu as a standard target. The documented server installation paths use the PBS installer or Debian. I’d previously avoided this cross-distribution approach on the workstation hosting my Coder control plane in Episode 4. A spare, console-accessible node was a different tradeoff. It also leaves me responsible for package compatibility when upgrading it.

PBS came up on port 8007. The agent created a datastore named backup as a directory on the existing ext4 filesystem. No disk reformat and no ZFS pool. That kept the install small, though sharing the root filesystem means backup growth competes with the operating system for free space. A 128 GB drive makes that worth watching.

On the ThinkCentre, the agent registered the new PBS storage target and pinned its TLS certificate fingerprint. A reachable port wasn’t enough. The client needed to trust the server and authenticate to the datastore before the setup counted.

Then came the test I cared about. Back up the smaller Uptime Kuma container first, then the Home Assistant VM. The session reported a successful container backup in 26 seconds and a Home Assistant backup in 2 minutes 16 seconds. Those are operation times from the session, not storage benchmarks. The VM’s 32 GiB virtual disk includes sparse space; its size isn’t a claim about how many bytes crossed the network.

The manual backup invocation:

vzdump 100 101 --storage chromebox-pbs --mode snapshot

The agent configured both guests for a daily 02:30 backup job. More importantly, it restored the Home Assistant backup to a temporary VM ID on the ThinkCentre. The restore completed in about 90 seconds, and the scratch VM was removed afterward.

That established that the backup could reconstruct a VM without overwriting production. The scratch VM stayed stopped. I didn’t test booting it, reconnecting the radios, or controlling devices from it. Useful evidence, with a clear boundary. It also isn’t a complete off-site backup strategy. The Chromebox adds another physical machine in the same house, not protection from every failure the house could have.

Moving AdGuard Exposed the DNS Dependency I’d Forgotten

AdGuard was the second service, and the reason this modest box deserved an always-on role. DNS doesn’t need to compete with model inference or video analysis for the most impressive machine. It needs a stable address, a reachable service, and a place in the network that doesn’t disappear whenever I restart Home Assistant’s host.

The agent installed AdGuard Home directly on Ubuntu as a systemd service. Its admin interface used port 3000; DNS used port 53. Avoiding a container runtime here was consistent with the rest of the decision. Two services on an existing Linux host didn’t need another platform around them.

The web interface came up before DNS did. A login page proves an HTTP service is reachable. It doesn’t prove the resolver can bind its sockets or answer a client’s question.

AdGuard’s DNS listener hit an address already in use error. Ubuntu’s systemd-resolved stub was already listening on port 53 on loopback. A wildcard listener can conflict with a service bound to a specific local address; seeing different addresses in a socket listing doesn’t make the port available to every bind configuration.

The session disabled the resolved stub listener and arranged independent upstream resolution for the host itself. The key setting was:

[Resolve]
DNSStubListener=no

That line wasn’t the entire fix. Resolver configuration had to change too, so the host wouldn’t keep sending requests to a stub it had just stopped. The AdGuard FAQ documents this conflict. My session used a static host resolver configuration with public upstreams, while LAN clients could use AdGuard. The point was to free port 53 without leaving the DNS machine unable to resolve names itself.

After the change, the agent confirmed that AdGuard owned the DNS listener, answered queries, and served its authenticated admin interface. Filtering/protection was enabled. I don’t have an ad-blocking percentage or a whole-network migration report from this session, and I’m not inventing one. A successful DNS query to the new resolver was the immediate acceptance test.

Next I asked whether the old AdGuard instance on the ThinkCentre had been disabled. It hadn’t been the focus of the installation work. The agent stopped that LXC and set its Proxmox onboot flag to zero, preserving its data for rollback. The new Chromebox service remained running.

At that point, an inventory looked reassuring. The services responded. The old container was intentionally stopped. Active services had startup configuration. But checking the local AI router revealed a different story.

The router answered HTTP requests, yet its health payload said:

{
  "ok": false,
  "healthy_nodes": [],
  "healthy_models": []
}

Its model list was empty too. The first interpretation was that the model backends were unavailable. I pushed back because I was having a conversation powered by DeepSeek at that moment. That didn’t prove the router’s own path worked; another client path could still reach the model. It did make “the model must be down” an incomplete explanation.

The decisive test ran from inside the router’s container. Resolving a backend hostname timed out. The backend address in the router configuration used a .local name, and the container’s resolver still pointed to the AdGuard instance I’d just retired.

I asked whether moving DNS had provided the timing clue. It had. The services weren’t independent in the way the earlier inventory implied. The old AdGuard container had been part of the router’s route to its backend names. Stopping it exposed that dependency.

(Side quest: Why were .local hostnames used in the router code at all? Turns out that convention came from adding the NVIDIA DGX Sparks. NVIDIA’s connection workflow uses multicast DNS, or mDNS, and .local hostnames. Since the router was coded when they were added to the network, that particular agentic session locked on to mDNS as a homelab convention. It ignored prior art around static IP addressing. Just a harsh reminder that vibe coding across sessions and model types introduces gremlins.)

The failure chain was short once I could see it:

Retire old AdGuard container
  -> router container still uses the old DNS address
  -> backend hostname resolution times out
  -> node health checks fail
  -> model discovery returns an empty list

I reserve addresses for the homelab machines, so I chose those addresses for the router’s backend connections. The agent also replaced the dead resolver entry in the router container and restarted the service. Those were two related repairs: fix the backend destinations and stop the container depending on a retired DNS service.

For illustration, with example addresses substituted:

Router dependencyBeforeAfter
Spark backendA .local hostname requiring resolutionReserved address such as 192.0.2.4
Workstation backendAnother .local hostnameReserved address such as 192.0.2.41
Container resolverRetired AdGuard addressA live resolver configuration

The next check reported two healthy nodes and three healthy models. Model discovery populated again. I didn’t need to rewrite health reporting to make it optimistic. I needed the path being checked to work.

I’m not turning that into a rule that all infrastructure should use hardcoded IPs. In this small, manually reserved fleet, it removed a fragile dependency I understood. In another environment, managed DNS could be the better answer. The broader lesson is to inspect consumers before retiring a resolver. Migrating the service and migrating everything that depends on it are separate tasks.

I Finished by Making the Box Behave Like a Server

The Chromebox still booted into Ubuntu’s desktop. That was useful during installation, but unnecessary for a headless DNS and backup node. I asked the agent to switch it to console boot and investigate the startup menu delay.

The systemd change was straightforward:

systemctl set-default multi-user.target
systemctl disable --now gdm3

The agent verified the default target and display-manager configuration. GRUB was already configured with a hidden menu and zero timeout, so there wasn’t a newly measured bootloader speedup to claim. The session changed the startup configuration; it didn’t record a timed cold-boot test.

I then asked for an inventory across both boxes. I wanted to know what still ran, what had moved, and whether anything had been orphaned. That final pass was more valuable than another installation success message.

MachineKept runningDeliberately stopped or excluded
ThinkCentreHome Assistant OS, Uptime Kuma, homelab dashboard, local agent routerOld AdGuard LXC stopped with automatic startup disabled
ChromeboxPBS and AdGuard Home, enabled for startupDesktop startup disabled; no Proxmox VE, staging Home Assistant, speech layer, or NVR

The checks covered service status, boot-enablement flags, LAN reachability, and an actual DNS answer. Authentication challenges and login redirects established reachable endpoints, not successful backups by themselves. The backup and restore tests supplied that separate evidence.

The cleanup pass also found packages installed alongside PBS. Postfix was configured for local mail delivery, not an alert destination I was reading. I asked to disable it. ZFS tooling was present, but no pool existed; I kept it because I might experiment later. Neither choice changed the ext4-backed datastore into something else.

Disabling unused mail isn’t the same as configuring useful backup alerts. The scheduled jobs remained, but a notification path I would notice was still future work. So were a fuller recovery drill, observing an actual reboot, and watching retention and free-space growth over time. A fresh setup shouldn’t acquire a fictional operating history just because its services are enabled.

Finally, the agent updated the durable plan, machine inventory, and homelab skill. The documentation included the DNS migration lesson and the stopped old container. Future sessions needed to know that the old DNS address was retired, not rediscover it by breaking resolution again.

That is why DNS was the right use case for this Chromebox. Its contribution wasn’t winning a processor comparison. It moved a foundational service away from the main host and gave backups a separate destination. The most instructive failure happened during the move, when a dependency I’d forgotten became visible.

I started with a spare computer and a vague desire to use it. I finished with two specific jobs, a tested restore path, and a better understanding of which pieces of the homelab depend on which others. That’s a useful amount of ambition for an old Chromebox.

By the Numbers

  • 1 physical write-protect screw removed before the firmware conversion could proceed.
  • 4 cores / 8 threads, 16 GB RAM, and a 128 GB NVMe SSD in the Chromebox.
  • 2 intended roles on the new node: PBS backup storage and AdGuard Home DNS.
  • 0 Proxmox VE installations needed on the Chromebox.
  • 2 Proxmox guests included in the daily 02:30 backup job.
  • About 90 seconds for the reported restore to a stopped scratch VM, which was then removed.
  • 2 healthy router nodes and 3 healthy models reported after repairing the DNS dependency.

Comments