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.
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.
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.
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.
Portainer for the Docker web UI. Dozzle for live container logs. Watchtower pulls fresh images and restarts containers on a nightly schedule.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.