-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdocker-compose.yml
More file actions
311 lines (298 loc) · 12.9 KB
/
Copy pathdocker-compose.yml
File metadata and controls
311 lines (298 loc) · 12.9 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
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
name: prax
services:
# ── Prax (all-in-one: Flask + TeamWork + Qdrant + Neo4j + ngrok) ──
prax:
build:
context: .
dockerfile: Dockerfile
additional_contexts:
teamwork: ${TEAMWORK_PATH:-../teamwork}
prax-sandbox: ${PRAX_SANDBOX_PATH:-../prax-sandbox}
ports:
- "3000:8000" # TeamWork web UI
- "8000:8000" # TeamWork API (for Vite dev proxy)
- "5001:5001" # Prax API
- "4040:4040" # ngrok dashboard
env_file: .env
environment:
# Overrides applied by entrypoint-combined.sh, but also set here
# so `docker compose config` shows the full picture.
- RUNNING_IN_DOCKER=true
- TEAMWORK_ENABLED=true
- SANDBOX_HOST=sandbox
- BROWSER_CDP_URL=http://sandbox:9223
# Override WORKSPACE_DIR from .env — inside the container the mount
# point is always /app/workspaces regardless of the host path.
- WORKSPACE_DIR=./workspaces
# User identity — sandbox scoped to this user's workspace.
- PRAX_USER_ID=${PRAX_USER_ID:-}
# Ollama — only used when EMBEDDING_PROVIDER=ollama (local-llm profile).
- OLLAMA_BASE_URL=${OLLAMA_BASE_URL:-http://ollama:11434}
# OTel exporter — sends traces to Tempo when observability is enabled.
- OTEL_EXPORTER_OTLP_ENDPOINT=http://tempo:4318
- GRAFANA_URL=${GRAFANA_URL:-http://localhost:3002}
# Grant Docker socket access for sandbox exec.
# GID 0 = root group, which owns the socket on macOS Docker Desktop.
group_add: ["0"]
volumes:
- ${WORKSPACE_DIR:-../workspaces}:/app/workspaces
- ./static:/app/static
- ./logs:/app/logs
# Persistent data for embedded services — scoped per-user.
- ${WORKSPACE_DIR:-../workspaces}/${PRAX_USER_ID}/.services/qdrant:/data/qdrant
- ${WORKSPACE_DIR:-../workspaces}/${PRAX_USER_ID}/.services/neo4j:/data/neo4j
- ${WORKSPACE_DIR:-../workspaces}/${PRAX_USER_ID}/.services/teamwork:/data/teamwork
# Docker socket — required for sandbox exec.
# SECURITY: grants host Docker access — use only in trusted environments.
- /var/run/docker.sock:/var/run/docker.sock
# Dev: mount source for live reload without rebuilding the image.
- ./prax:/app/prax
- ./app.py:/app/app.py
- ./scripts:/app/scripts
# Git repo — required for self-improve worktree isolation.
- ./.git:/app/.git:ro
- ./tests:/app/tests:ro
depends_on:
sandbox:
condition: service_healthy
healthcheck:
test: ["CMD", "curl", "-sf", "http://localhost:5001/health"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
# ── Sandbox (pure execution env: Python stack + Chrome/CDP + desktop) ──
# Image source lives in the sibling prax-sandbox repo (override with
# PRAX_SANDBOX_PATH). `docker compose up --build` builds it automatically.
# No model API keys are passed in: the image ships no coding-agent CLI, so
# nothing inside calls a model — and a keyless container gives a code-exec
# tool nothing to exfiltrate. No compose-level healthcheck either: the
# image's own HEALTHCHECK (`pgrep -x supervisord`) is what the `prax`
# service's `condition: service_healthy` waits on (the old one curled
# OpenCode's :4096, which no longer exists, so `prax` could never start).
# Mirrors prax-sandbox's own compose.
sandbox:
build:
context: ${PRAX_SANDBOX_PATH:-../prax-sandbox}/sandbox
image: prax-sandbox:latest
restart: unless-stopped
volumes:
# User-scoped workspace — only this user's folder is mounted, and nothing
# else from the host: no repo source, no persisted /root (both existed
# for the removed coding agents, and gave the sandbox write access to
# the harness's own code).
- ${WORKSPACE_DIR:-../workspaces}/${PRAX_USER_ID}:/workspace
# ── Secrets proxy (KEYLESS Prax — endorsed security path) ─────────
# Opt-in: docker compose --profile secrets-proxy up
# Runs the sibling praxagent/prax-secrets-proxy repo as its OWN container, so a
# compromised/injected Prax cannot reach the real keys (separate container = the
# isolation). The REAL keys live ONLY in that repo's gitignored .env — NOT Prax's.
# To route Prax through it, set in Prax's .env (with PLACEHOLDER keys):
# OPENAI_BASE_URL=http://secrets-proxy:8785/openai
# ANTHROPIC_BASE_URL=http://secrets-proxy:8785/anthropic
# See docs/security/secrets-proxy.md.
secrets-proxy:
profiles: ["secrets-proxy"]
build:
context: ${PRAX_SECRETS_PROXY_PATH:-../prax-secrets-proxy}
image: prax-secrets-proxy:latest
restart: unless-stopped
# The real keys come from the PROXY repo's own .env — deliberately NOT Prax's
# .env, so the isolation is real (this container never sees Prax's config).
env_file: ${PRAX_SECRETS_PROXY_PATH:-../prax-secrets-proxy}/.env
environment:
- PROXY_HOST=0.0.0.0
- PROXY_PORT=8785
ports:
- "127.0.0.1:8785:8785"
healthcheck:
test: ["CMD", "python", "-c",
"import urllib.request,sys; sys.exit(0 if urllib.request.urlopen('http://localhost:8785/healthz').status==200 else 1)"]
interval: 15s
timeout: 5s
retries: 3
start_period: 10s
# ── Ollama (local embeddings + LLM) ───────────────────────────────
# Optional — start with: docker compose --profile local-llm up
ollama:
image: ollama/ollama:latest
profiles: ["local-llm"]
restart: unless-stopped
ports:
- "11434:11434"
volumes:
- ${WORKSPACE_DIR:-../workspaces}/.ollama:/root/.ollama
healthcheck:
test: ["CMD-SHELL", "bash -c 'echo > /dev/tcp/localhost/11434'"]
interval: 10s
timeout: 5s
retries: 3
start_period: 10s
# Sidecar that waits for Ollama to be healthy, then pulls the
# configured embedding model so users don't have to do it manually.
ollama-init:
image: curlimages/curl:latest
profiles: ["local-llm"]
depends_on:
ollama:
condition: service_healthy
restart: "no"
entrypoint: ["/bin/sh", "-c"]
command:
- |
echo "Pulling embedding model: ${EMBEDDING_MODEL:-nomic-embed-text}..."
curl -s http://ollama:11434/api/pull -d "{\"model\": \"${EMBEDDING_MODEL:-nomic-embed-text}\"}"
echo ""
echo "Model pull complete."
# ── Observability Stack ───────────────────────────────────────────
# Start with: COMPOSE_PROFILES=observability docker compose up
# Or: docker compose --profile observability up
# Tempo — distributed tracing backend (receives OTLP spans from Prax)
tempo:
image: grafana/tempo:2.7.2
profiles: ["observability"]
command: ["-config.file=/etc/tempo.yaml"]
volumes:
- ./observability/tempo.yaml:/etc/tempo.yaml:ro
- tempo-data:/var/tempo
ports:
- "3200:3200" # Tempo query API
- "4317:4317" # OTLP gRPC
- "4318:4318" # OTLP HTTP
# Cap the container's own json-file stdout logs so they can't fill the disk
# on a long-running stack. Anchor reused by the other observability services.
logging: &obs-logging
driver: json-file
options:
max-size: "10m"
max-file: "3"
# Loki — log aggregation (receives logs from Promtail)
loki:
image: grafana/loki:3.4.2
profiles: ["observability"]
# Our config adds a compactor + 7-day retention (the image default keeps logs
# forever). See observability/loki.yaml.
command: ["-config.file=/etc/loki/loki.yaml"]
ports:
- "3100:3100"
volumes:
- ./observability/loki.yaml:/etc/loki/loki.yaml:ro
- loki-data:/loki
logging: *obs-logging
# Promtail — ships Docker container logs to Loki
promtail:
image: grafana/promtail:3.4.2
profiles: ["observability"]
command: ["-config.file=/etc/promtail/config.yaml"]
volumes:
- ./observability/promtail.yaml:/etc/promtail/config.yaml:ro
- /var/run/docker.sock:/var/run/docker.sock:ro
- /var/lib/docker/containers:/var/lib/docker/containers:ro
# Host logs from a NATIVELY-run stack (`make run-local-all`). In the
# all-Docker path this dir is empty/absent, so the prax-native scrape job
# tails nothing — harmless. :ro so promtail can only read.
- ./.local-run:/var/log/prax-native:ro
depends_on:
- loki
logging: *obs-logging
# Prometheus — metrics (scrapes /metrics from Prax)
prometheus:
image: prom/prometheus:v3.2.1
profiles: ["observability"]
command:
- "--config.file=/etc/prometheus/prometheus.yml"
- "--storage.tsdb.path=/prometheus"
- "--storage.tsdb.retention.time=30d"
volumes:
- ./observability/prometheus.yaml:/etc/prometheus/prometheus.yml:ro
- prometheus-data:/prometheus
# Lets the prometheus.yaml `host.docker.internal:5001` target resolve to the
# host, so a NATIVELY-run Prax (`make run-local-all`) is scrapeable. Harmless
# in the all-Docker path (the in-network `prax:5001` target is used instead).
extra_hosts:
- "host.docker.internal:host-gateway"
ports:
- "9090:9090"
logging: *obs-logging
# Grafana — unified dashboards (traces + logs + metrics)
# Bound to host port 3002 so it doesn't conflict with the tailscale
# sidecar (which binds :3001 on the tailnet interface for remote HTTPS
# access). Laptop users hit https://<host>.ts.net:3001/ via the
# sidecar; local-only users hit http://localhost:3002 directly.
grafana:
image: grafana/grafana:11.5.2
profiles: ["observability"]
environment:
- GF_SECURITY_ADMIN_USER=admin
- GF_SECURITY_ADMIN_PASSWORD=prax
- GF_AUTH_ANONYMOUS_ENABLED=true
- GF_AUTH_ANONYMOUS_ORG_ROLE=Admin
volumes:
- ./observability/grafana/provisioning:/etc/grafana/provisioning
- grafana-data:/var/lib/grafana
ports:
- "3002:3000"
depends_on:
- tempo
- loki
- prometheus
logging: *obs-logging
# ── Tailscale sidecar (private HTTPS access from your laptop) ─────
# Opt-in. To enable, add to .env:
# TS_AUTHKEY=tskey-auth-... (reusable, NON-ephemeral, pre-approved
# — get one from
# https://login.tailscale.com/admin/settings/keys
# ephemeral keys count against the free
# tier's 1,000-min/month minute budget)
# COMPOSE_PROFILES=tailscale (compose reads this from .env, so the
# sidecar starts automatically with
# `docker compose up`; without it the
# service is silently skipped)
# Kernel TUN mode — required so `tailscale serve` can dial upstream
# services on the docker network (e.g. prax:8000). In userspace mode
# the serve proxy lives inside netstack and can't reach 172.x docker
# IPs. Needs cap_add NET_ADMIN + /dev/net/tun on the host (standard
# on Linux; Docker Desktop on macOS has it too).
# State lives in a Docker volume so the node identity survives restarts
# and re-registers as the same device (avoids burning device slots).
# Routes: https://<host>.ts.net/ → prax:8000 (TeamWork UI)
# https://<host>.ts.net:3001/ → grafana:3000 (Observability)
tailscale:
image: tailscale/tailscale:latest
profiles: ["tailscale"]
# Container hostname is intentionally NOT set to ${TS_HOSTNAME}: doing
# that would put `<eth0-ip> prax` in this container's /etc/hosts and
# shadow Docker's DNS entry for the `prax` service — outbound proxy
# dials to prax:8000 would loop back to the sidecar itself and 502.
# Tailnet hostname is controlled by TS_HOSTNAME below; the Docker-level
# name doesn't need to match.
hostname: tailscale-sidecar
cap_add:
- net_admin
devices:
- /dev/net/tun:/dev/net/tun
environment:
- TS_AUTHKEY=${TS_AUTHKEY:?TS_AUTHKEY is required when the tailscale profile is active — see https://login.tailscale.com/admin/settings/keys}
- TS_HOSTNAME=${TS_HOSTNAME:-prax}
- TS_STATE_DIR=/var/lib/tailscale
- TS_SERVE_CONFIG=/config/serve-config.json
- TS_EXTRA_ARGS=${TS_EXTRA_ARGS:-}
volumes:
- tailscale-state:/var/lib/tailscale
- ./tailscale/serve-config.json:/config/serve-config.json:ro
restart: unless-stopped
healthcheck:
test: ["CMD", "tailscale", "status", "--peers=false"]
interval: 30s
timeout: 5s
retries: 3
start_period: 30s
volumes:
# Observability data — ephemeral, fine as Docker volumes.
# All persistent data (memory, models, agent configs) lives in WORKSPACE_DIR.
tempo-data:
loki-data:
prometheus-data:
grafana-data:
# Tailscale node state — keeps the same device identity across restarts.
tailscale-state: