# PRD prd-new: Containers inside a bottle - **Status:** Draft - **Author:** Claude - **Created:** 2026-07-21 - **Issue:** #392 ## Summary Let an agent run `docker` and `docker compose` *inside* its bottle by starting a guest-local rootless podman service that exposes a Docker-compatible API socket. Gated per bottle by `nested_containers: true`. No host daemon socket is mounted and no capability is added to the guest, on any backend. ## Problem Agent tasks routinely involve containers: `docker compose up` to verify a scaffolded stack, a throwaway database for a test run, building an image to check a Dockerfile works. Bottles have no way to do any of that, so those tasks either fail or get done outside the bottle — which defeats the point of the bottle. ## Goals / success criteria - A bottle with `nested_containers: true` on the `macos-container` backend can run `docker compose up -d --wait` against a stock compose file, pulling from a routed registry and serving from the workspace. - The agent's habits do not change: `docker` and `docker compose`, not `podman`. - No host Docker socket is mounted, on any backend. - No capability is added to the Apple Container guest. - Backends without a guest-local engine reject the flag with a clear error rather than ignoring it. - Bottles that do not set the flag carry none of its cost. ## Non-goals - Nested containers as an isolation layer. The single-UID mapping means `root` inside a nested container is the agent user outside it. This is for build and test workloads; the bottle stays the security boundary. - The `docker` and `firecracker` backends. Both reject the flag for now. - Reaching registries that are not routed through the bottle's egress proxy. ## Design ### Why podman, not rootless Docker Apple Container's capability bounding set omits `CAP_SYS_ADMIN`, which the kernel requires in order to write a multi-range `uid_map` via `newuidmap`. Rootless Docker has no path that avoids that write, so it cannot run in a bottle without granting `CAP_SYS_ADMIN` — which is close to root in practical terms and gives up most of what the bottle is for. Podman does have a path: with **no** subordinate UID range configured it falls back to a single-UID self-mapping, which an unprivileged process may write itself. The image build therefore *removes* the agent user's `/etc/subuid` and `/etc/subgid` entries rather than adding them — their presence is exactly what would send podman down the `newuidmap` path. Full negative result in [`docs/research/rootless-docker-in-apple-container-spike.md`](../research/rootless-docker-in-apple-container-spike.md). ### The flag, and its name The flag is kept because it gates real costs, not because podman needs a privilege grant: ~100MB of derived image, a resident service per bottle, and relaxed modes on `/dev/fuse` and `/dev/net/tun` that the majority of bottles should not get. It is named `nested_containers`, not `docker_access`. It implies no Docker and grants access to nothing on the host — naming it after Docker access would describe the one thing the design refuses to do. ### Shape - `bot_bottle/backend/macos_container/nested_containers.py` — derived-image build, guest env, device preparation, service start/readiness. - `bot_bottle/backend/macos_container/nested-containers-init.sh` — the unprivileged bootstrap that runs inside the bottle. Fails closed on every prerequisite it needs (podman, the storage/network helpers, writable device nodes, an *empty* subordinate range) rather than degrading. - The derived image `…-nested-containers` layers the Docker CLI, the compose plugin, `fuse-overlayfs`, `slirp4netns`, and `uidmap` onto the agent image. Podman itself already ships in every built-in image (#451). - `BottleBackend.supports_nested_containers` — false by default, so a backend that cannot honor the flag fails in the shared `prepare` template. Guest configuration the single-UID mapping forces: `ignore_chown_errors` (no second UID for layers to be chowned to), `cgroups="disabled"` and `cgroup_manager="cgroupfs"` (no cgroup delegation reaches the guest), `events_logger="file"` (no journald socket). ### Registry reach Image pulls egress through the bottle's proxy like everything else, so each registry needs a route. Docker Hub and GHCR need `preserve_auth: true` — their token dance uses a client-fetched per-scope bearer token that the proxy would otherwise strip. Layer pulls should also set `dlp.outbound_detectors: false` and `dlp.inbound_detectors: false`: body scanning on a multi-hundred-MB layer is what triggers #455, and a registry route's scanned bodies are compressed layer blobs rather than anything a detector can read. ## Open questions - Whether the `docker` and `firecracker` backends want an equivalent, or whether guest-local containers stay macOS-only. - Outbound networking *from* a nested container (DNS, proxy env, CA) beyond what the compat socket gives the bottle itself.