Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. |
Expand Down
2 changes: 1 addition & 1 deletion docs/cli-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion logs.sh
Original file line number Diff line number Diff line change
@@ -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 "$@"
Loading