Extract the shared CA cert polling loop into `backend/util.poll_ca_cert`
(firecracker and macos backends were duplicating deadline/sleep/raise logic).
Each caller now wraps a fetch lambda and converts TimeoutError to its own
error type. Also corrects the PRD port publication line from {port}:{port}
to {host_port}:8099.
6.8 KiB
PRD prd-new: Consolidate infra backend for Docker
- Status: Active
- Author: Claude
- Created: 2026-07-20
- Issue: #431
Summary
The Docker backend runs two containers — bot-bottle-orch-gateway (gateway
data plane) and bot-bottle-orchestrator (control plane) — where the
macOS and Firecracker backends already run a single combined infra
unit. This PRD collapses Docker to the same model: one bot-bottle-infra
container running both processes under the gateway_init supervise tree, a
restructured Dockerfile.infra as the shared gateway+orchestrator base,
and a handful of extracted shared utilities (CA cert polling, teardown
sequence, launch skeleton) that are currently duplicated across all three
consolidated_launch.py files.
Goals / success criteria
- Docker backend starts exactly one infra container instead of two.
Dockerfile.infrais the shared base image (gateway + orchestrator, no buildah); the Firecracker image layers buildah on top of it.- The orchestrator process runs under the
gateway_initsupervise tree inside the combined container (one PID-1, one restart/health surface). - CA cert polling, the teardown sequence, and the shared launch skeleton (ensure-infra → register → provision → return context) live in a single shared module; all three backends import from it.
- No functional change to macOS or Firecracker launch paths.
Non-goals
- Changing per-bottle isolation — agents stay one-VM/container-each.
- Consolidating transport implementations (
DockerGatewayTransport,AppleGatewayTransport,SshGatewayTransport) — these are already the right abstraction boundary. - macOS DHCP-inversion of registration order — irreducible backend difference, stays as-is.
- Any changes to the orchestrator RPC protocol or the attribution model.
Design
Dockerfile restructuring
Current shape:
Dockerfile.gateway— data plane (mitmproxy, gitleaks, git, openssh, supervise daemons)Dockerfile.orchestrator— control plane (python:3.12-slim + bot_bottle package; stdlib-only, no third-party deps)Dockerfile.infra— Firecracker only:FROM bot-bottle-gateway+ buildah +COPY --from bot-bottle-orchestrator
New shape:
Dockerfile.gateway— unchangedDockerfile.orchestrator— unchanged (single definition of orchestrator content; both Docker infra and Firecracker infraCOPY --fromit)Dockerfile.infra— shared base:FROM bot-bottle-gateway+COPY --from bot-bottle-orchestrator(no buildah — Docker infra image)Dockerfile.infra.fc— Firecracker only:FROM bot-bottle-infra+ buildah/crun/netavark/aardvark-dns (layered on the shared base, same net result as today)
The comment in Dockerfile.infra that says "the docker backend keeps
orchestrator + gateway as separate images; this combined image exists only
for the Firecracker single-VM cut" is removed.
Orchestrator in the supervise tree
gateway_init already supervises the data-plane daemons (egress, git-http,
supervise-MCP). The orchestrator control plane is added as another supervised
process: python3 -m bot_bottle.orchestrator --host 0.0.0.0 --port <port> --broker stub.
The orchestrator source is bind-mounted (/app → repo root, as today) so
dev live-reload still works. source_hash-based container recreation in
OrchestratorService.ensure_running continues to apply — a code change
recreates the combined infra container, which bounces both gateway and
orchestrator. This is acceptable: the docker backend is a dev/legacy target
where in-flight egress connections across a code deploy are not a hard
requirement.
OrchestratorService changes
OrchestratorService currently starts two containers in sequence: gateway
first (DockerGateway.ensure_running), then orchestrator. After this PRD:
- Single
docker runofbot-bottle-infra:latest - Container name:
bot-bottle-infra(replacesbot-bottle-orch-gateway+bot-bottle-orchestrator) - Published ports:
127.0.0.1:{host_port}:8099for the control plane (gateway_initlistens on a fixed internal port 8099; the caller-chosen host port maps to it) - Bind mounts: repo root + host root (same as today)
DockerGatewaybecomes an implementation detail ofOrchestratorServicerather than a separately started container; the gateway image name (GATEWAY_IMAGE) is no longer referenced at runtime, only at build time for theDockerfile.infrabase
The _gateway() / ensure_running two-step in OrchestratorService is
replaced by a single _run_infra_container().
Shared backend utilities
Three items are duplicated across
backend/docker/consolidated_launch.py,
backend/macos_container/consolidated_launch.py, and
backend/firecracker/consolidated_launch.py:
-
CA cert polling loop —
deadline = time.monotonic() + timeout; while ...: try fetch CA; sleep— extracted tobackend/consolidated_util.py:poll_ca_cert(transport, *, timeout). -
Teardown sequence —
OrchestratorClient(url).teardown_bottle(id)+deprovision_git_gate(transport, id)— extracted tobackend/consolidated_util.py:teardown_consolidated(url, transport, bottle_id). -
Launch skeleton — all three follow: ensure-infra → allocate/register → provision git-gate → fetch CA cert → return launch context. The macOS inversion (agent starts before registration, source IP from DHCP) is the only deviation. Extract a shared
_provision_bottle(transport, bottle_id, plan, orchestrator_url)helper covering the register → provision → return-token steps; the backends keep their ownlaunch_consolidatedwrappers for the before/after (infra-ensure + agent-start + IP allocation), calling the shared helper.
The new backend/consolidated_util.py module holds only backend-neutral,
transport-agnostic logic. All three backends import from it.
Implementation chunks
-
(this PR) Dockerfile restructuring: rename current
Dockerfile.infracontent toDockerfile.infra.fc; write newDockerfile.infraas gateway+orchestrator base. Update Firecracker image-build references fromDockerfile.infra→Dockerfile.infra.fc. -
Add orchestrator process to
gateway_initsupervise tree. -
Collapse
OrchestratorServiceto a single-container start; rename container frombot-bottle-orch-gateway/bot-bottle-orchestrator→bot-bottle-infra; update image name constant. -
Extract
backend/consolidated_util.pywithpoll_ca_cert,teardown_consolidated, and_provision_bottle; update all threeconsolidated_launch.pyfiles to import from it. -
Update tests that reference the old container names or two-container startup sequence.
Open questions
None — the supervise-tree approach and shared Dockerfile layering were confirmed in issue #431.