The Philosophical Developer — Chapter 56: The Lab Gets A Control Plane
2026-10-05 · 5 min read

The AI lab had grown into nine services and the ports had become folklore. 3000 here, 8188 there, a template formula that computed 12432 for the second spare GPU — a number that belonged to no scheme anyone could name. Every port was written in six places: the quadlet, the installer, the detection script, the README, the docs, and a service’s CORS list. Change one and you got to keep the outage.
This chapter is about the week I stopped hand-editing and gave the lab a control plane: one registry, one CLI, one port scheme, and a security audit pass while I was in there.
The registry: services.json as the only writer
The rule is now absolute: services.json is the only writer for ports and service lists. Every entry carries the unit name, container name, host port, container port, bind address, health endpoint, tier, image (digest-pinned where it matters), boot flag, and — this one earned its keep — the auth mode: none, api-key, account, or token.
Everything else reads from it. The quadlets are generated from it at install time. The docs tables are generated from it. The bar widget polls it. When I want to know what a service’s exposure actually is, the answer is one jq away, not six greps with a coin flip.
This is the same traceability discipline from the early chapters, applied to infrastructure: one source of truth, everything else derived, drift becomes a diff instead of a mystery.
The port scheme: two families, one story
The renumbering gave the lab a scheme a human can hold in their head:
- 1143x — model APIs, Ollama-adjacent: llama.cpp main on 11435, research on 11436, Strata on 11434. Spare-GPU routers get
11438 + index— after I fixed the old formula, which produced 11431, 12432, 13433 and proved that unverified arithmetic in a template is just a bug with extra steps. - 31xx — web apps, sequential: Open WebUI 3100, ComfyUI 3101, Sketch Lab 3102, HyperFrames 3103, the optional Hermes gateway 3104, DeepSeek Harness 3105 — loopback-only, on purpose.
11437 is intentionally unused after Strata moved into the 11434 slot. I left the gap. Filling gaps for tidiness is how port schemes rot; a hole with a story is better than a number without one.
The exposure model is written down now instead of being vibes: trusted home LAN, plain HTTP, nothing port-forwarded, Strata key-gated on every request, the agent harness reachable only from the machine itself. If the trust model ever changes, the registry says exactly what to rebind.
The CLI and the bar widget
scripts/ai-lab is the control plane: status, info, start, stop, restart, toggle, ports, url — all driven by the registry, with JSON output so the machine clients and I use the same door.
$ ./scripts/ai-lab status
SERVICE STATE TIER PORT HEALTH
strata running optional 11434 up (200)
comfyui running image 3101 up (200)
sketchlab running web 3102 up (200)
...
The Ryoku top-bar plugin (ailab) polls ai-lab status --json and toggles services from the panel — same commands, no terminal needed to stop ComfyUI when I’m not rendering. The plugin contract got a ground rule too: author in the repo, install with ryoku plugin add, never hand-edit the installed copy. The repo copy is the truth; the installed copy is a build artifact.
The audit pass, because cleanup is a security event
Renumbering and consolidating touches every config file in the repo — which is exactly when secrets leak into commits. So the cleanup plan carried a security phase, and it paid for itself:
- Real API keys and
WEBUI_SECRET_KEYlive inconfig/*/service.envfiles that are gitignored; only*.examplefiles are tracked. Verified, not assumed. - Caddy is retired as a runtime component: its config and quadlet stay in the repo for reference, never deployed, never started. The README says so in plain words, because a file that looks deployable will eventually be deployed by someone tired.
- The DeepSeek Harness went back to stock npm
dshwithNetwork=hostand its own loopback bind — the TCP bridge and the client patches are deleted, and a ground rule forbids re-adding them. A patched fork of an upstream tool is a debt you pay in silence. - One live bug the audit surfaced: the harness’s router refused its entire provider settings section at boot when any hand-declared provider had an empty models list. One bad route, silently zero routes. The fix documents itself in
settings.yaml.example: every hand-declared provider needs its models list, or comment it out.
What this pattern is for
None of this is clever. Registry, generator, CLI, audit — it is the same loop I ran on code in the TDD chapters, pointed at the homelab: make the truth explicit, derive everything else, verify with a command instead of a memory. The lab now rehydrates on a fresh Arch box with one curl | bash, boots the way the registry says it boots, and answers to one verb set.
The next time the lab grows an organ — and it will, Strata just moved in last week — it is a registry entry, not a scavenger hunt through six files.
The series: The Philosophical Developer — real experiments, real code, real outcomes. Previous: Chapter 55: The Model That Outruns My Reading Speed. Earlier in this arc: Chapter 45: AI Lab Quadlets — The Packaged Version.
Repo:
- ai-lab-quadlets — the stack, the registry, the CLI, the audit trail