ADR-server-005
Container Launch โ docker run as server mode's second launch vehicle; server_image declaration; framework-owned container lifecycle
Context
Server mode (ADR-server-001) launches a nodeset's auto_host subprocess on a host interpreter (server_python, usually a conda env). Some systems cannot be a conda env at all: a compiled C++ SLAM stack (ORB-SLAM3) whose build products don't pip-install, or a GPL codebase (pySLAM) that must stay out of the framework's process and env. The one pre-existing containerized nodeset, model_pyslam, proved containers work but at the wrong layer: it registered as a local-mode nodeset and ran a private bridge โ a hand-written _client.py (docker run, port pick, health wait, GPU fallback) plus an in-container FastAPI shim โ invisible to the registry, the scheduler, GPU admission, multi-worker fan-out, and the /mcp projection. Every future containerized system would have re-written that ~800-line bridge.
Decision
Make the container a launch vehicle of server mode, not a nodeset's private mechanism. Server mode now has two launch vehicles: Native Launch (host subprocess on server_python โ the default) and Container Launch (docker run <server_image>). Both run the same stock auto_host and serve the same manifest protocol, so everything downstream of BaseServer โ proxy-node generation, env-panel bridge, worker fan-out, admission bookkeeping, /mcp โ is shared and unchanged.
- Declaration: four
BaseNodeSetClassVars โserver_image(the trigger; wins overserver_python),server_image_gpu(CDI device flag, one CPU-degrade retry),server_mounts(extra binds; missing host paths skip, not fail),server_container_python. - No framework code in images: the repo root is bind-mounted read-only at a fixed mount point; the registry rewrites command-line paths,
PYTHONPATH, and loopback executor URLs (โhost.docker.internal) under it (registry.py ยท _build_container_server). Images carry only the model's deps plusrequirements-serve.txt. - Lifecycle is framework-owned:
ContainerServer(BaseServer)(container_server.py) runs a foregrounddocker run --rm --initas a direct child (BaseServer health/timeout/pdeathsig apply to the client, which proxies SIGTERM), names containers deterministically and reaps a stale namesake before start (the client-SIGKILL orphan case), stops viadocker stopfirst, and always targets the rootless daemon explicitly.
Consequences: a containerized nodeset is one ClassVar plus a Dockerfile โ first user model_orbslam3 (see the nodeset page); model_pyslam's private bridge is now legacy, migration pending. Full mechanism: Container Launch design doc.