Repository navigation
Expand file tree
/
Copy pathcompose.example.yml
More file actions
313 lines (303 loc) · 14.9 KB
/
Copy pathcompose.example.yml
File metadata and controls
313 lines (303 loc) · 14.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
312
313
# Docker Compose for the croit AIplane stack.
#
# A Docker-native alternative to the systemd-podman Quadlets in
# `deploy/quadlet/` — same images, same config, for hosts that run Docker /
# Docker Desktop rather than rootful podman.
#
# Services:
# aiplane AIplane itself (OpenAI-compatible proxy + agent runtime + UI)
# google-workspace-mcp per-user Gmail/Calendar/Drive/… (backs the "Google
# Workspace" connector; profile: google-workspace);
# see docs/connectors.md
# sandbox-runner code-execution runner (profile: sandbox)
# egress-proxy allowlisting squid for networked sandbox runs
# (profile: sandbox)
# ocr-sidecar PDF-aware Unlimited-OCR adapter (profile: ocr)
#
# SANDBOX ISOLATION: the runner spawns each job as a sibling container via the
# host Docker socket with `--runtime=runsc` (gVisor). Unlike remote *podman*,
# the Docker daemon honors `--runtime` over its socket, so this is REAL
# isolation — but only if gVisor (runsc) is registered as a Docker runtime on
# the host (see docs/sandbox.md). The runner self-checks at boot and logs
# `SANDBOX IS NOT ISOLATED` if the runtime didn't apply. On Docker Desktop /
# macOS, where gVisor isn't available, you can set SANDBOX_RUNTIME=local-unsafe
# for dev only (NO isolation) — never in a deployment.
#
# Full setup walkthrough: deploy/README.md. Relative paths below resolve against
# this file's directory (deploy/), so env/config files live in deploy/.
#
# ── Setup (from the repo root) ──────────────────────────────────────────────
# printf 'AIPLANE_SESSION_KEY=%s\n' "$(openssl rand -hex 32)" > deploy/aiplane.env
# docker compose -f deploy/compose.example.yml up -d aiplane
#
# Then open http://localhost:8080 — the setup wizard asks for your OIDC provider,
# tests the login for real, and hands you an admin account. No config file.
#
# Locked out later (IdP moved, no group maps to admin)? Reopen the wizard
# without disturbing anyone still using the gateway:
# docker compose -f deploy/compose.example.yml exec aiplane restore-setup
# It prints a one-time URL, valid for 30 minutes. See docs/auth.md.
#
# Every other service sits behind a profile, so the commands above need
# nothing but aiplane.env. A profile's prerequisites are checked only when it
# is enabled.
#
# For the Google Workspace connector, additionally:
# cp deploy/quadlet/google-workspace-mcp.example.env deploy/google-workspace-mcp.env
# $EDITOR deploy/google-workspace-mcp.env
#
# ── Local testing on Docker Desktop (recommended) ──────────────────────────
# Run ONLY the Workspace MCP here and keep the gateway native (`mise run dev`).
# That avoids a split-horizon URL problem: the browser AND a native gateway both
# reach the MCP at http://localhost:8000, so WORKSPACE_EXTERNAL_URL can simply be
# http://localhost:8000 (set it in google-workspace-mcp.env).
#
# docker compose -f deploy/compose.example.yml --profile google-workspace up google-workspace-mcp
#
# Then in the (native) gateway: /admin/connectors → Google Workspace → URL
# http://localhost:8000/mcp (no trailing slash) → Save → Enable, then
# /tools/integrations → Connect.
#
# ── Full stack (Linux server, behind a TLS reverse proxy) ──────────────────
# Put a reverse proxy in front (it terminates TLS for both the gateway and the
# MCP server on their own hostnames). Set WORKSPACE_EXTERNAL_URL to the MCP's
# PUBLIC https URL — there the browser and the gateway resolve it to the same
# place, so no split-horizon. Then:
#
# docker compose -f deploy/compose.example.yml --profile google-workspace up -d # gateway + mcp
# docker compose -f deploy/compose.example.yml --profile sandbox up -d # + sandbox + egress proxy
name: aiplane
services:
aiplane:
image: ghcr.io/croit/aiplane:latest
restart: unless-stopped
# Loopback only — front with a TLS-terminating reverse proxy in production.
ports:
- "127.0.0.1:8080:8080"
# Holds AIPLANE_SESSION_KEY — the one variable a deployment must set.
# Everything else (pools, models, groups, OIDC) is configured in the browser
# and stored in the database.
env_file:
- aiplane.env
environment:
RUST_LOG: info,aiplane=info,aiplane_core=info,aiplane_features=info,aiplane_agents=info,aiplane_runtime=info,aiplane_tools=info,aiplane_api=info
volumes:
# The image's AIPLANE_DATA_DIR=/var/lib/gateway already puts the SQLite
# database and the RAG index store on this volume — mounting it is all
# that persistence needs.
#
# The gateway has no config file: topology, groups, the OIDC provider
# and every operator setting live in the database and are edited in the
# web UI. Everything below is a property of where the process runs.
# host.docker.internal.
- aiplane-data:/var/lib/gateway
read_only: true
tmpfs:
- /tmp
cap_drop:
- ALL
security_opt:
- "no-new-privileges:true"
# PDF-aware OCR adapter. It accepts the gateway's internal /ocr multipart
# contract, converts PDFs with PyMuPDF, and calls a separately deployed
# Unlimited-OCR vLLM server. Profile: ocr.
ocr-sidecar:
build:
context: ./ocr-sidecar
dockerfile: Containerfile
profiles:
- ocr
restart: unless-stopped
expose:
- "9100"
environment:
# Required with this profile. Not `:?`: compose interpolates every
# service, enabled or not, so that would block an AIplane-only start.
# The sidecar refuses to boot when it is empty.
VLLM_BASE_URL: ${OCR_VLLM_BASE_URL:-}
VLLM_API_KEY: ${OCR_VLLM_API_KEY:-}
OCR_MODEL: ${OCR_MODEL:-baidu/Unlimited-OCR}
# One inference call per page (default) keeps page numbers and survives a
# single page failing. Set to 1 for one multi-image call per document:
# cheaper, but the answer has no page structure. See docs/ocr.md.
OCR_MULTI_IMAGE: ${OCR_MULTI_IMAGE:-0}
read_only: true
tmpfs:
- /tmp
security_opt:
- "no-new-privileges:true"
google-workspace-mcp:
image: ghcr.io/taylorwilsdon/google_workspace_mcp:latest
profiles:
- google-workspace
restart: unless-stopped
# No `command:` override — the image's default CMD already runs
# `uv run main.py --transport streamable-http` (its ENTRYPOINT is
# `/bin/sh -c`, so overriding command breaks arg parsing). The CMD wrapper
# appends `--tool-tier`/`--tools` from the TOOL_TIER / TOOLS env vars below.
# Loopback only — for the full stack, expose it publicly via the reverse
# proxy (the OAuth consent redirects through it in the user's browser).
ports:
- "127.0.0.1:8000:8000"
# GOOGLE_OAUTH_CLIENT_ID/SECRET, WORKSPACE_EXTERNAL_URL,
# WORKSPACE_MCP_ALLOWED_CLIENT_REDIRECT_URIS,
# FASTMCP_SERVER_AUTH_GOOGLE_JWT_SIGNING_KEY.
env_file:
- google-workspace-mcp.env
# OAuth state MUST outlive the container. This server's FastMCP OAuth proxy
# keeps *everything* in one store: the gateway's dynamically-registered
# client, authorization codes, the refresh tokens it issues and the upstream
# Google tokens. Unconfigured it writes them under $HOME in the container's
# writable layer, which is gone after `docker compose up --force-recreate` or
# an image update — and then every user is disconnected within one
# access-token lifetime (~30 min) with `invalid_client: Invalid client_id` on
# the next refresh. So keep it on a named volume.
volumes:
- gworkspace-mcp-oauth:/var/lib/gworkspace-mcp
environment:
MCP_ENABLE_OAUTH21: "true"
WORKSPACE_MCP_STATELESS_MODE: "true"
WORKSPACE_MCP_PORT: "8000"
TOOL_TIER: core # core | extended | complete (consumed by the image CMD).
# `extended` adds get_gmail_attachment_content — needed to download mail
# attachments. The gateway stores returned files as conversation artifacts,
# so their bytes never enter the model's context.
# Persist the OAuth store on the volume above.
WORKSPACE_MCP_OAUTH_PROXY_STORAGE_BACKEND: disk
WORKSPACE_MCP_OAUTH_PROXY_DISK_DIRECTORY: /var/lib/gworkspace-mcp/oauth-proxy
# Keep the rootfs read-only: redirect uv's cache/home onto the /tmp tmpfs
# (the entrypoint runs `uv run`, which needs a writable cache dir).
UV_CACHE_DIR: /tmp/uv-cache
XDG_CACHE_HOME: /tmp
# NOTE: no `read_only: true` here — this image's entrypoint runs `uv run`,
# which builds an editable install under /app at startup and needs a
# writable rootfs. The other hardening (non-root app user, stateless
# sessions, no-new-privileges, loopback-only) still applies.
tmpfs:
- /tmp
security_opt:
- "no-new-privileges:true"
# GitLab (self-managed / Community Edition) bridge — backs the "GitLab
# (self-managed / CE)" connector. GitLab's native MCP is a Duo/Premium feature,
# so CE uses this community bridge (zereight/gitlab-mcp) in streamable-HTTP +
# remote-authorization mode: each request carries the user's own GitLab token,
# which the bridge forwards to GitLab — so it needs NO public URL and NO OAuth
# (unlike the Google MCP). Profile: gitlab.
gitlab-mcp:
image: zereight050/gitlab-mcp:latest
profiles:
- gitlab
restart: unless-stopped
# Loopback publish is only for a native (mise run dev) gateway to reach it at
# http://localhost:3333/mcp; the full-stack AIplane uses http://gitlab-mcp:3002/mcp.
ports:
- "127.0.0.1:3333:3002"
# GITLAB_API_URL=https://<your-gitlab>/api/v4 (deployment-specific)
env_file:
- gitlab-mcp.env
environment:
HOST: "0.0.0.0"
STREAMABLE_HTTP: "true" # serves /mcp; required for remote-authorization
REMOTE_AUTHORIZATION: "true" # per-request token: each user's PAT, not a fixed one
# DNS-rebinding guard: the bridge auto-allows only loopback + its bound
# address, so it 403s ("Host header is not allowed") when the full-stack
# gateway reaches it by service name. Allow that Host (must match the
# connector URL authority). Loopback (native-dev :3333) is always allowed.
MCP_ALLOWED_HOSTS: "gitlab-mcp:3002"
GITLAB_PERMISSION_MODE: "readonly" # or `modify` (no deletes) / `full` (read+write)
security_opt:
- "no-new-privileges:true"
# Discord MCP bridge — backs the seeded *global* Discord connector. A Discord
# bot token authenticates one shared bot for the whole gateway (there's no
# per-user Discord account to connect, unlike Slack/GitHub/Atlassian), so the
# connector's scope is "global": an admin enables it and points it at this
# bridge's URL in /admin/connectors. The croit/discord-mcp image (our fork of
# SaseQ/discord-mcp — full member-roster cache + fuzzy member search; channel
# + DM tools) defaults to stdio; SPRING_PROFILES_ACTIVE=http (below) makes it
# serve streamable HTTP on :8085 at /mcp. Profile: discord.
# Member lookup needs the "Server Members Intent" enabled in the Discord
# Developer Portal (Bot → Privileged Gateway Intents), else the roster never
# loads and searches come back empty.
discord-mcp:
image: ghcr.io/croit/discord-mcp:latest
profiles:
- discord
restart: unless-stopped
# Defaults to stdio; this makes it serve streamable HTTP on :8085.
environment:
SPRING_PROFILES_ACTIVE: http
# Loopback publish is only for a native (mise run dev) gateway to reach it
# at http://localhost:3334/mcp; the full-stack AIplane reaches it by service
# name at http://discord-mcp:8085/mcp. Never expose this port publicly — the
# bot token grants full bot access, not scoped per caller.
ports:
- "127.0.0.1:3334:8085"
env_file:
- discord-mcp.env
security_opt:
- "no-new-privileges:true"
# Code-execution sandbox runner (run_in_sandbox / generate_document /
# capture_webpage). Spawns each job as a single-use sibling container via the
# host Docker socket under gVisor (runsc) — real isolation, provided runsc is
# registered as a Docker runtime on the host (docs/sandbox.md → "Installing a
# sandbox runtime"). The boot self-check logs `isolation confirmed` or a loud
# `SANDBOX IS NOT ISOLATED`; treat the latter as a hard stop.
#
# SECURITY: mounting docker.sock is host-root-equivalent, and this service
# runs arbitrary model-generated code — keep it on an internal network, never
# expose port 9000, and only grant the sandbox tools deliberately (RBAC).
sandbox-runner:
image: ghcr.io/croit/aiplane-sandbox-runner:latest
profiles:
- sandbox
restart: unless-stopped
# Reachable by the gateway over the compose network as http://sandbox-runner:9000.
# Point the gateway at it under Code sandbox at /admin/settings:
# sandbox.enabled = on, sandbox.runner_url = http://sandbox-runner:9000
# (a restart applies it — the field is marked accordingly).
# NOT published to the host — it executes untrusted code.
expose:
- "9000"
volumes:
# Drives the host Docker daemon to spawn the gVisor sandboxes (siblings).
- /var/run/docker.sock:/var/run/docker.sock
environment:
SANDBOX_BIND: "0.0.0.0:9000"
SANDBOX_PODMAN: docker # drive the Docker CLI (image ships it), not podman
SANDBOX_RUNTIME: runsc # gVisor; must be a registered Docker runtime on the host
SANDBOX_IMAGE: ghcr.io/croit/aiplane-sandbox:latest
RUST_LOG: info,sandbox_runner=info
# Egress for networked runs (pip / capture_webpage). The runner attaches
# sandboxes to this network — use the *Docker* network name, which compose
# prefixes with the project: "<project>_sandbox-egress" (project = name:).
SANDBOX_EGRESS_NETWORK: aiplane_sandbox-egress
SANDBOX_EGRESS_PROXY: http://egress-proxy:3128
networks:
- default # to pull images / be reached by the gateway
depends_on:
- egress-proxy
# Allowlisting egress proxy for networked sandbox runs. The runner attaches
# networked sandboxes to `sandbox-egress` (internal, no direct internet); their
# only way out is this proxy, which forwards only to hosts in allowlist.txt.
egress-proxy:
image: docker.io/ubuntu/squid:latest
profiles:
- sandbox
restart: unless-stopped
volumes:
- ./quadlet/squid.conf:/etc/squid/squid.conf:ro
- ./quadlet/allowlist.txt:/etc/squid/allowlist.txt:ro
networks:
- default
- sandbox-egress
networks:
# `internal: true` is the point — sandboxes attached here have no direct route
# out; their only path is the egress proxy (which also sits on the default
# network). Default-deny egress.
sandbox-egress:
internal: true
volumes:
aiplane-data:
# Google Workspace MCP OAuth state — encrypted at rest by the server, but it
# holds live Google refresh tokens. Deleting it forces every user to reconnect.
gworkspace-mcp-oauth: