Repository navigation
Expand file tree
/
Copy pathdocker-compose.yml
More file actions
226 lines (217 loc) · 11.7 KB
/
Copy pathdocker-compose.yml
File metadata and controls
226 lines (217 loc) · 11.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
# DuploCloud Extension Developer Kit — local platform from PUBLISHED images.
#
# Nothing here builds from source. Every service pulls a published image, pinned by a tag in .env.
# Start with ./run.sh (pulls + brings the stack up). Build/deploy extensions with scripts/.
#
# Image tags (see .env): the UI is tagged by its git SHA; the studio + agent by the pipeline's
# deploy_tag. Set the matching feature-branch tags in .env before ./run.sh.
services:
mongo:
image: mongo:7.0.14
restart: unless-stopped
ports:
- "${MONGO_PORT:-27017}:27017"
environment:
- MONGO_INITDB_ROOT_USERNAME=authuser
- MONGO_INITDB_ROOT_PASSWORD=authpass
- MONGO_INITDB_DATABASE=auth-service
volumes:
# loaded_extensions records live here → previously-loaded extensions are replayed on restart.
- mongo_data:/data/db
# Vector database backing the Knowledge Base. Always on, like mongo — one code path, and the
# Knowledge Base is simply present. Reachable in-network at http://qdrant:6333; the host port is
# published on loopback so the Qdrant dashboard is available for debugging.
qdrant:
image: qdrant/qdrant:v1.18.2
restart: unless-stopped
ports:
# Bound to 127.0.0.1 deliberately, unlike the mongo line above: the fixed API key below is only
# safe because nothing off this host can reach Qdrant, and the dev kit is documented as running on
# EC2 hosts. A bare "6333:6333" would publish it on every interface.
- "127.0.0.1:${QDRANT_PORT:-6333}:6333"
environment:
# Fixed, and deliberately not a secret — Qdrant is reachable only on the compose network and
# loopback, per the port binding above. Single-sourced from .env so the compose file and
# scripts/register-qdrant.sh cannot drift.
- QDRANT__SERVICE__API_KEY=${QDRANT_API_KEY:-devkit-local-qdrant}
# Required, not decorative: lets the studio mint read-only per-collection tokens for the agent
# instead of handing it this master key.
- QDRANT__SERVICE__JWT_RBAC=true
volumes:
- qdrant_data:/qdrant/storage
# One-shot: chown the shared named volumes to uid 1001 so the non-root studio + agent
# (both appuser 1001) can write them. Named volumes are created root-owned, and run.sh
# does `down -v` each start, so this must run every startup (see the services' depends_on).
# Both processes writing as 1001 keeps ticket/skill/extension dirs interoperable.
init-perms:
image: busybox:1.36
command: ["sh", "-c", "chown -R 1001:1001 /data /extstudio /csessions && echo 'volumes chowned to 1001'"]
volumes:
- platform_data:/data
- extension_studio_data:/extstudio
- claude_sessions:/csessions
duplo-ai-studio:
image: quay.io/duplocloud/backend:${STUDIO_TAG}
# Feature-branch studio images are built amd64-only (arm64 is built on main). On Apple Silicon
# this runs under emulation. Override per-arch by setting STUDIO_PLATFORM in .env.
platform: ${STUDIO_PLATFORM:-linux/amd64}
restart: unless-stopped
ports:
- "${STUDIO_PORT:-60021}:60021"
env_file:
- ./.env
environment:
- MongoDB__ConnectionString=mongodb://authuser:authpass@mongo:27017/?retryWrites=false
- MongoDB__DatabaseName=duplo-ai-helpdesk
- ASPNETCORE_URLS=http://+:60021
# Published-image paths (Dockerfile.core stages resources at /app; SDK feed + extension-studio dir).
- AIStudio__ResourcesSourcePath=/app/resources
- AIStudio__SdkFeedPath=/app/sdk
- AIStudio__ExtensionStudioPath=/app/extension-studio
# Browser reaches xterm on the published host port, so this must be host-reachable.
- AIStudio__XtermHost=http://localhost:${XTERM_PORT:-6060}
- LocalFileStorage__RootPath=/data
volumes:
# Shared /data file store (skills + loaded-extension skill files). Shared with the agent so it can
# read the seeded skills. Survives restart.
- platform_data:/data
# Hot-loaded extension bundles unpack to AIStudio:ExtensionStudioPath. Named volume → extensions survive a
# container recreate; ExtensionReplayHostedService re-registers the Active records on boot.
- extension_studio_data:/app/extension-studio
depends_on:
mongo:
condition: service_started
init-perms:
condition: service_completed_successfully
claude-code-agent:
image: quay.io/duplocloud/duplo-agent:${AGENT_TAG}
restart: unless-stopped
ports:
- "${AGENT_PORT:-8000}:8000"
environment:
- PERSISTENT_VOLUME_STORAGE=/data
- AGENT_CALLBACK_BASE_URL=http://duplo-ai-studio:60021
- DCAF_TOOL_CALL_LIMIT=15
- DCAF_DEFAULT_TOOLKIT=true
- ENABLE_WEB_SEARCH=true
- WEB_SEARCH_BACKEND=auto
- APPROVAL_CALLBACK_TIMEOUT=50
- MCP_TIMEOUT=1800
- CCA_DISABLE_BWRAP=true
- CLAUDECODE=''
# ── LLM provider (agent picks by precedence: ANTHROPIC_API_KEY → gateway (ANTHROPIC_BASE_URL
# without a key) → Azure Foundry → CLAUDE_CODE_OAUTH_TOKEN → AWS Bedrock). Set the one you use
# in .env; the rest stay empty. ──
- CLAUDE_MODEL=${CLAUDE_MODEL:-claude-sonnet-5}
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY:-}
# Your own Claude Code subscription (`./run.sh --model subscription`). Sits next-to-last in the
# agent's precedence chain, deliberately: a token exported in a shell must never displace a
# provider someone configured on purpose. Local development only — it authenticates as one person.
- CLAUDE_CODE_OAUTH_TOKEN=${CLAUDE_CODE_OAUTH_TOKEN:-}
# LLM gateway (OpenRouter, Bifrost, LiteLLM, Snowflake Cortex …). The CLAUDE_CODE_* knobs are
# optional per-gateway tuning the agent forwards to the Claude CLI only when set.
- ANTHROPIC_BASE_URL=${ANTHROPIC_BASE_URL:-}
- ANTHROPIC_AUTH_TOKEN=${ANTHROPIC_AUTH_TOKEN:-}
- CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS=${CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS:-}
- CLAUDE_CODE_MAX_CONTEXT_TOKENS=${CLAUDE_CODE_MAX_CONTEXT_TOKENS:-}
- CLAUDE_CODE_AUTO_COMPACT_WINDOW=${CLAUDE_CODE_AUTO_COMPACT_WINDOW:-}
- AZURE_BASE_URL=${AZURE_BASE_URL:-}
- AZURE_API_KEY=${AZURE_API_KEY:-}
- AZURE_CLIENT_ID=${AZURE_CLIENT_ID:-}
- AWS_REGION=${AWS_REGION:-}
- AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID:-}
- AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY:-}
- AWS_SESSION_TOKEN=${AWS_SESSION_TOKEN:-}
volumes:
# Shared /data so the agent reads the skills the studio seeded (CCA reads /data/skills).
- platform_data:/data
# Agent runs non-root (uid 1001, HOME=/home/appuser) — persist sessions under its real home,
# not /root (which the 1001 process can't write).
- claude_sessions:/home/appuser/.claude
depends_on:
init-perms:
condition: service_completed_successfully
duplo-ui:
image: quay.io/duplocloud/duplo-ai-helpdesk-ui:${UI_TAG}
restart: unless-stopped
ports:
- "${UI_PORT:-4200}:80"
volumes:
# Published image serves the SPA only; this conf proxies API prefixes to the studio (same-origin, no CORS).
- ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
depends_on:
- duplo-ai-studio
xterm:
image: quay.io/duplocloud/duplo-xterm:${XTERM_TAG:-main-d323e0b}
restart: unless-stopped
privileged: true
ports:
- "${XTERM_PORT:-6060}:6060"
environment:
- CORS_ORIGIN=*
# Build toolchain for extensions (.NET SDK 8 + Node 22). Behind a profile so `docker compose pull`
# and `up` in run.sh skip it entirely — a user who only runs the platform never downloads it. The
# build wrapper starts it on demand with `docker compose run --rm --no-deps builder`.
#
# Being a compose service (rather than a bare `docker run`) is what lets the build reach the studio over
# the compose network — the SDK feed comes from AIStudio at http://duplo-ai-studio:60021 — but only when
# the studio is in THIS compose project. `docker compose` is project-scoped (the project defaults to the
# directory name), so a platform started from a second checkout or a git worktree — which is how this
# repo is developed — is not on this network and that name does not resolve. scripts/_builder.sh detects
# which case it is and falls back to the published host port via host.docker.internal (see extra_hosts
# below). Both routes are live; the compose network is simply the preferred one.
builder:
profiles: [tools]
# scripts/_builder.sh resolves BUILDER_TAG and exports BUILDER_IMAGE before every build. This default is only
# for a hand-run `docker compose run builder`, and must match _BUILDER_PINNED_IMAGE there.
image: ${BUILDER_IMAGE:-quay.io/duplocloud/duplo-extension-builder@sha256:8185e6a74379ae99e145f55608b3bc17e3037e95e64a612a226fe0eb15c0dbcd}
# Unset by default, so the build runs on the host's native architecture. Set BUILDER_PLATFORM only
# to force one (e.g. linux/amd64 if a publish ever ends up amd64-only) — an unset value is dropped
# by compose, whereas copying duplo-ai-studio's linux/amd64 default would silently emulate a .NET
# build on Apple Silicon.
platform: ${BUILDER_PLATFORM:-}
# The caller's real uid/gid, exported by scripts/_builder.sh — so dist/ ends up owned by the
# invoking user, not root. The default is only a fallback for a hand-run `docker compose run`.
user: "${DUPLO_UID:-1000}:${DUPLO_GID:-1000}"
working_dir: /work
# Lets the build reach a studio that is NOT in this compose project via the published host port.
# compose scopes services per project (the directory name), so a stack started in another checkout
# is not on this project's network and duplo-ai-studio does not resolve; _builder.sh detects that and
# points DUPLO_BASE at host.docker.internal instead. A no-op on Docker Desktop, where the name
# already resolves; on Linux it is what makes it resolve at all.
extra_hosts:
- "host.docker.internal:host-gateway"
volumes:
- .:/work
# Persist the package caches across builds — a cold build downloads the whole SDK closure and
# the Angular dependency tree; a warm one should reuse both. Mount ONLY at paths the image
# pre-creates world-writable, and see build/Dockerfile.builder for why (it is the file that has to
# change if a cache path is ever added, and it explains the per-uid volume names below).
- nuget_cache:/cache/nuget
- npm_cache:/cache/npm
environment:
- DUPLO_BUILD_NATIVE=1
- DUPLO_TARGET=${DUPLO_TARGET:-local}
- DUPLO_BASE=${DUPLO_BASE:-}
# scripts/_target.sh accepts these straight from the environment, with no .env at all — which is
# exactly how CI runs (secrets, DUPLO_TARGET=remote, no file). Without forwarding them the build
# would reach _target.sh inside the container and die claiming DUPLO_HOST is "unset in .env",
# naming a file the value never came from. `:-` so an unset var stays empty instead of failing
# interpolation; an empty value is what _target.sh already treats as absent.
- DUPLO_HOST=${DUPLO_HOST:-}
- DUPLO_TOKEN=${DUPLO_TOKEN:-}
- DUPLO_ADMIN_TOKEN=${DUPLO_ADMIN_TOKEN:-}
volumes:
mongo_data:
platform_data:
extension_studio_data:
claude_sessions:
qdrant_data:
# 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
# relaxing the umask instead is not an option. Reclaim them with `docker volume ls | grep duplo_devkit`.
nuget_cache:
name: duplo_devkit_nuget_${DUPLO_UID:-1000}
npm_cache:
name: duplo_devkit_npm_${DUPLO_UID:-1000}