diff --git a/docker-compose.yml b/docker-compose.yml index 9b2574c..6ac43e1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -127,6 +127,10 @@ services: - AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID:-} - AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY:-} - AWS_SESSION_TOKEN=${AWS_SESSION_TOKEN:-} + # Docker-in-Docker: the dind service shares this container's network namespace, so its daemon + # is reachable on loopback here and nowhere else. + - DOCKER_HOST=tcp://127.0.0.1:2375 + - CCA_ENABLE_DOCKER=true volumes: # Shared /data so the agent reads the skills the studio seeded (CCA reads /data/skills). - platform_data:/data @@ -137,6 +141,46 @@ services: init-perms: condition: service_completed_successfully + # Docker-in-Docker daemon for the agent, mirroring the dind sidecar in the helpdesk-helm chart. + # + # network_mode joins claude-code-agent's network namespace — the compose equivalent of a pod's + # shared netns — so dockerd bound to 127.0.0.1 is reachable by the agent and by nothing else: not + # the compose network, not the host. The agent keeps owning its network identity, so + # http://claude-code-agent:8000 still resolves for the studio. The agent cannot depends_on this + # service (network_mode already makes this one depend on the agent); docker calls simply fail for + # the few seconds until the daemon is up. + # + # The image entrypoint is bypassed: with TLS off it force-binds tcp://0.0.0.0:2375 and ignores any + # --host passed. modprobe replicates the entrypoint's kernel-module loading; --tls=false suppresses + # the insecure-bind startup delay. + dind: + image: docker:27-dind + restart: unless-stopped + privileged: true + network_mode: "service:claude-code-agent" + entrypoint: ["/bin/sh", "-c"] + command: + - | + modprobe ip_tables || true + modprobe ip6_tables || true + modprobe nf_tables || true + modprobe overlay || true + exec dockerd --host=tcp://127.0.0.1:2375 --tls=false + environment: + - DOCKER_HOST=tcp://127.0.0.1:2375 + healthcheck: + test: ["CMD", "docker", "info"] + interval: 10s + start_period: 5s + volumes: + # Same /data as the agent, so `docker run -v /data/...` bind-mount paths resolve daemon-side. + - platform_data:/data + # Image/container storage. Wiped with the rest by run.sh's `down -v`, like the chart's emptyDir. + - dind_storage:/var/lib/docker + depends_on: + claude-code-agent: + condition: service_started + duplo-ui: image: quay.io/duplocloud/duplo-ai-helpdesk-ui:${UI_TAG} restart: unless-stopped @@ -216,6 +260,7 @@ volumes: extension_studio_data: claude_sessions: qdrant_data: + dind_storage: # Keyed by uid on purpose: these are written as the calling user (see the service's `user:`), and one # uid's runtime-created cache subdirectories are not writable by another, so sharing a single pair # would break the second uid to build. build/Dockerfile.builder has the full reasoning, including why diff --git a/docs/architecture.md b/docs/architecture.md index e846f0f..1dea2c8 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -12,6 +12,7 @@ Nothing here builds from source. Every service pulls a published image pinned by | `duplo-ai-studio` | `quay.io/duplocloud/backend` | 60031 | The platform API, serving `/v1/aiservicedesk`. Owns extensions, workspaces, resources, and tickets, and enforces the license in `Licensing__Token`. | | `duplo-ui` | `quay.io/duplocloud/duplo-ai-helpdesk-ui` | 4210 | The Angular portal. The image serves the SPA only; the mounted `nginx/default.conf` proxies API prefixes to the studio, so the browser sees one origin and there is no CORS. | | `claude-code-agent` | `quay.io/duplocloud/duplo-agent` | 8010 | The LLM agent that executes provisioning tickets. Calls back to the studio at `http://duplo-ai-studio:60021`. | +| `dind` | `docker:27-dind` | — | Docker-in-Docker daemon for the agent, mirroring the helpdesk-helm chart's sidecar. Shares `claude-code-agent`'s network namespace and listens on its loopback only (`DOCKER_HOST=tcp://127.0.0.1:2375`), so the agent can run containers and nothing else can reach the daemon. Mounts the same `/data` so bind-mount paths match. Runs privileged. | | `mongo` | `mongo:7.0.14` | 27018 | All platform state, including the `loaded_extensions` records. Matches the version pinned in the helpdesk-helm chart. | | `qdrant` | `qdrant/qdrant:v1.18.2` | 6333 | The vector database behind the Knowledge Base. The studio reaches it in-network at `http://qdrant:6333`; the host port is published so its dashboard is available for debugging. Runs with RBAC on, so the studio can mint read-only per-collection tokens instead of sharing the master key. | | `xterm` | `quay.io/duplocloud/duplo-xterm` | 6061 | In-browser terminal. The browser reaches it on the published host port, so `AIStudio__XtermHost` must be host-reachable. | diff --git a/docs/cli-reference.md b/docs/cli-reference.md index e706a21..75ae325 100644 --- a/docs/cli-reference.md +++ b/docs/cli-reference.md @@ -107,7 +107,7 @@ extensions, Mongo, the file store, and the Knowledge Base vectors are lost. ``` A thin wrapper around `docker compose logs -f`; every argument is passed straight through. Services are -`duplo-ai-studio`, `claude-code-agent`, `duplo-ui`, `mongo`, `qdrant`, and `xterm`. +`duplo-ai-studio`, `claude-code-agent`, `dind`, `duplo-ui`, `mongo`, `qdrant`, and `xterm`. ## What each command destroys diff --git a/logs.sh b/logs.sh index 5394c5a..c906a29 100755 --- a/logs.sh +++ b/logs.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# Tail logs. Optional service name: duplo-ai-studio | claude-code-agent | duplo-ui | mongo | xterm +# Tail logs. Optional service name: duplo-ai-studio | claude-code-agent | dind | duplo-ui | mongo | xterm set -euo pipefail cd "$(dirname "$0")" exec docker compose logs -f "$@"