home  /  the rig

The rig.

Two Linux boxes on the LAN, talking to each other over encrypted tunnels. One is a tablet where I experiment with user interfaces for LLM-layer apps. The other is a headless docker host where the services run. Everything ships from and to these two machines.

ui playground

Surface Pro 9 — the tablet

The Surface was always the fun device. It's a tablet and a laptop and a pen-input machine with a detachable keyboard. That's an unusually rich surface for experimenting with LLM-layer UIs — voice, stylus, photo, on-screen keyboard, GPS, all colocated. Most of the apps here started as experiments in what that interaction feels like.

  • OSUbuntu 24.04
  • Kernellinux-surface 6.18.7
  • DesktopGNOME · Wayland
  • AudioPipeWire (not PulseAudio)
  • Camerasrear ov13858 · front ov5693 (kernel gap)
  • ISPIntel IPU6
  • GPSSurface modem + GeoClue
  • RadioHackRF One + PortaPack H2 over USB-C
  • InputType Cover · Surface Slim Pen 2
deployment platform

dockerllm — the docker host

A single Debian box that hosts the containers. Every project lives in its own directory under /home/benny/projects/, ships as one or more containers from its own docker-compose.yml, and is iterated on through the same session-based workflow as everything else. No Kubernetes, no global orchestrator. "Bring this compose file up" is the unit of deployment.

  • OSDebian · Docker
  • OpsPortainer · Dozzle · Watchtower
  • BackupsVolumerize → Backblaze B2 (nightly)

What runs on the Docker host

Every project follows a near-identical shape — its own directory, its own docker-compose.yml, its own CLAUDE.md and PRACTICES.md, its own session-based change log. That consistency is what makes the box feel like a playground rather than a pile of one-offs.

management

Ops control plane

Portainer for the Docker web UI. Dozzle for live container logs. Watchtower pulls fresh images and restarts containers on a nightly schedule.

volumerize

Offsite backups, nightly

blacklabelops/volumerize (duplicity) runs on a nightly schedule with encrypted diffs to Backblaze B2. 30-day retention, forced full every 7. The whole box can be reconstructed from B2 alone.

my_mcps

"Real-time world" MCP monorepo

One ASGI process, one container, multiple MCP servers mounted behind a single port. Weather (Open-Meteo + RainViewer radar) and alerts (travel-safety digest). Stateless, media-first, bind-mounted TOML config.

budget

Personal budget web app

Small Flask + SQLite app. Bind-mounted app/ and data/ so it can be iterated on without rebuilds. The "quick and happy" tier of the platform.

what it took

Problems overcome — the Surface side

camera

libcamera 0.7 + IPU6 from source

Ubuntu 24.04 ships libcamera 0.2, which doesn't handle the Surface's IPU6 ISP. Built libcamera 0.7 from source into /usr/local/, wrote a custom IPA tuning file with measured black level and AWB, and added a udev rule to keep the sensors powered on — they don't auto-wake. The rear 13 MP ov13858 now streams at 30 fps through GStreamer.

on-screen keyboard

Detach the Type Cover, the OSK appears

A tiny Python daemon watches pyudev and toggles GNOME's built-in OSK based on whether a physical keyboard is attached. Uses evdev capability checks to filter out touchscreens, mice, and media-key-only devices that would otherwise masquerade.

1.5 s debounce on removal (the Type Cover fold fires multiple events). Listener on login1 PrepareForSleep re-scans 3 s after resume.

app-audio capture

PipeWire graph tee without a loopback hack

With PipeWire running, I can capture the audio of any app — Firefox playing a lecture, a Zoom call, mpv — by finding its stream in pw-dump and tee-ing it into my own recorder. No virtual audio device, no system-wide routing change, no stereo mix voodoo.

pinned TLS

Every request encrypted, pinned to the server I trust

The client and the server identify each other by a pinned certificate fingerprint, so traffic on the LAN is TLS-wrapped to a server the client already recognises — nothing in the middle can talk to either side. The bundle calls window.pywebview.api.* shims that route every request through a pinned httpx client in Python, keeping the crypto in code I own rather than in the browser's CA store.

streaming transcripts

LocalAgreement-style aggregation for Deepgram jitter

Live transcripts jitter — the tail of a partial often gets rewritten on the next token. A LocalAgreement-style aggregator promotes tokens to "stable" only when they've persisted across N consecutive partials. The UI shows stable tokens committed; the flickering tail renders lighter.

radio

HackRF + PortaPack Mayhem over ChibiOS shell

The PortaPack H2 runs Mayhem firmware on a ChibiOS/RT shell over USB-CDC. Different VID/PID from stock HackRF — meaning standard hackrf_* tools can't see it in PortaPack mode. I built the serial-control library by interrogating the device command-by-command and writing down what each one actually does.

on the server side

Problems overcome — the Docker host

deploy unit

One compose file per project

No Kubernetes, no Helm, no cluster. Every project on the box has its own docker-compose.yml and the deploy ritual is docker compose up -d --build. Portainer gives a single pane of glass over what's running.

bootstrap

Every repo teaches the LLM how it wants to be worked on

Each project ships a .claude/ directory with per-project slash commands and settings, plus a CLAUDE.md that orients a fresh session — where you are, what's shipped, what the next commit should be. A terminal at the project root is enough to pick up.

backups

Reconstructable from Backblaze B2 alone

The volumerize container walks databases, logs, prompts, container data, and the compose files themselves. If the box died tomorrow, a fresh Debian install plus a duplicity restore is the recovery procedure.

pairing the two boxes

Traffic wrapped in TLS and SSH

The Surface is a consumer of the Docker host, not a build target. magicnotesurface on the Surface dials the my_mcps container on dockerllm; every hop is over an encrypted tunnel. Deploys move via SSH, API calls move via TLS. The network itself is the integration layer — and everything crossing it is wrapped.

LAN topology: Surface Pro 9 on the left and the dockerllm Debian box on the right, both joined through a LAN switch. TLS padlocks on HTTP arrows between them, SSH padlocks on deploy arrows. Cloud services (Backblaze B2, Anthropic API) reach dockerllm only.

Two boxes, one operating model

The Surface and dockerllm run the same disciplined workflow — CLAUDE.md, PRACTICES.md, session branches, red tests, layer-discipline enforcement. Moving a project between them is a docker compose up, not a port. That's what makes the pair feel like one shop.