-
Notifications
You must be signed in to change notification settings - Fork 4
Expand file tree
/
Copy pathdocker-compose.yml
More file actions
220 lines (212 loc) · 8.9 KB
/
Copy pathdocker-compose.yml
File metadata and controls
220 lines (212 loc) · 8.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
services:
# ── MySQL Database ────────────────────────────────────────────────────────────
db:
image: mysql:8.0
environment:
MYSQL_DATABASE: ${DB_NAME:-safi}
MYSQL_USER: ${DB_USER:-safi}
MYSQL_PASSWORD: ${DB_PASSWORD}
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-rootpassword}
volumes:
- db_data:/var/lib/mysql
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p${MYSQL_ROOT_PASSWORD:-rootpassword}"]
interval: 5s
timeout: 5s
retries: 12
restart: unless-stopped
# ── SAFi Application ──────────────────────────────────────────────────────────
app:
build: .
pull_policy: build
# Explicit tag so `purge` can reference it. Without this, compose names the
# image after the project directory, which differs between checkouts.
image: safi:latest
# Pinned, not left to the container id. An MCP tool server that stores
# credentials on the mount may derive its encryption key from the hostname
# (the Google Workspace extension salts with os.hostname()), and a hostname
# that changes on every `up` silently invalidates saved tokens, sending the
# operator back through an OAuth flow after each rebuild.
hostname: safi-app
ports:
- "${APP_PORT:-5000}:5000"
env_file: .env
environment:
# Override DB_HOST so the app connects to the compose db service, not localhost
DB_HOST: db
# MCP server definitions live on a MOUNT, never inside the image. The
# Dockerfile copies safi_app/, so a file under it is replaced on every
# `up --build` — which silently wiped an operator's installed servers and
# made scripts/safi_mcp.py useless in Docker.
MCP_SERVERS_JSON: /app/mcp/servers.json
depends_on:
db:
condition: service_healthy
volumes:
- ./logs:/app/logs
- ./cache:/app/cache
# Installed MCP tool servers. Mounted so `safi_mcp.py add` survives a
# rebuild; see mcp/README.md.
- ./mcp:/app/mcp
# npm's cache, so an `npx` tool server downloads once per deployment
# rather than on every container start. It has to be this path: the MCP
# SDK passes stdio servers only a safe allow-list of environment
# variables, so NPM_CONFIG_CACHE cannot redirect it, while HOME is
# inherited and npm defaults to $HOME/.npm.
- npm_cache:/root/.npm
# RAG indexes persist across rebuilds; the entrypoint auto-builds the
# small `safi` knowledge base here on first boot if it's missing.
- vector_store:/app/vector_store
# The host git checkout, read-only, so the TCB verify button can report
# which branch this deployment is on. The Dockerfile deliberately never
# copies .git, so release images stay clean and report no branch.
- ./.git:/app/.git:ro
restart: unless-stopped
# ── Retention purge (daily) ──────────────────────────────────────────────────
# Runs scripts/retention_purge.py once a day so per-org retention policies
# and legal holds actually execute. Bare-metal deployments use the systemd
# timer in deploy/systemd/ instead.
purge:
# Same build context and the SAME image tag as `app`, so the two services
# share one image instead of producing two identical multi-gigabyte copies
# for a script that imports argparse/json/re/sys/os/time/uuid/datetime/
# pathlib and safi_app.config.
#
# `build:` must stay here even though app already declares it: a service
# with only `image:` and no `build:` is something compose tries to PULL, so
# `docker compose up` fails with "pull access denied for safi" on any
# machine that has not already built the tag locally. With both keys the
# second build is a fully cached no-op resolving to the same image id.
build: .
pull_policy: build
image: safi:latest
env_file: .env
environment:
DB_HOST: db
SERVICE: purge
depends_on:
app:
condition: service_started
db:
condition: service_healthy
volumes:
- ./logs:/app/logs
restart: unless-stopped
# ── Scheduled Updates runner ─────────────────────────────────────────────────
# Backlog 54: runs each due schedule as a full governed turn and emails the
# approved output to the owner. Same image tag as `app` for the same
# one-build-one-image reason as `purge`.
scheduler:
build: .
pull_policy: build
image: safi:latest
env_file: .env
environment:
DB_HOST: db
SERVICE: scheduler
depends_on:
app:
condition: service_started
db:
condition: service_healthy
volumes:
- ./logs:/app/logs
restart: unless-stopped
# ── Knowledge-base indexer ───────────────────────────────────────────────────
# Builds user-created RAG corpora out of the request path. Same image tag as
# `app` and `purge` for the same reason: one build, one image.
#
# MUST mount the same `vector_store` volume as `app` — this service writes
# the indexes the app reads. Without it the build succeeds into the
# container's own layer and every agent keeps answering ungrounded, which
# looks like an indexing bug rather than a missing mount.
#
# ./cache is shared too so the indexer reuses the embedding model the app
# already downloaded instead of pulling its own copy.
indexer:
build: .
pull_policy: build
image: safi:latest
env_file: .env
environment:
DB_HOST: db
SERVICE: indexer
depends_on:
app:
condition: service_started
db:
condition: service_healthy
volumes:
- ./logs:/app/logs
- ./cache:/app/cache
- vector_store:/app/vector_store
restart: unless-stopped
# ── OAuth tool gateways (optional) ───────────────────────────────────────────
# Per-user Google and Microsoft 365 tools. Both are their own small
# authorization servers, not part of the app, which is why each gets its own
# env file rather than sharing SAFi's .env: an Entra client secret has no
# business in the app's environment.
#
# OFF BY DEFAULT, and that is the point. `profiles:` keeps them out of a
# plain `docker compose up`, so the majority who configure neither provider
# are unaffected. Start them with:
#
# docker compose --profile gateways up -d
#
# `required: false` on the env file matters just as much: without it compose
# refuses to parse this file at all when the env is absent, which would break
# `up` for every deployment that never wanted a gateway.
#
# TLS IS YOURS TO PROVIDE. GATEWAY_BASE_URL must be https, and these services
# publish plain HTTP. Point your existing terminator (the demo uses Apache)
# at the published port and set GATEWAY_BASE_URL to that public hostname.
# Compose deliberately has no proxy service; ingress stays the operator's.
workspace-gateway:
build: .
pull_policy: build
image: safi:latest
profiles: ["gateways"]
env_file:
- path: ./gateways/workspace-gateway.env
required: false
environment:
SERVICE: workspace-gateway
# Forced here, NOT left to the env file. The store holds the signing key,
# registered clients and every member's Google tokens; anywhere but this
# volume and `up --build` silently signs everyone out and sends them back
# through consent. Same lesson as the ./mcp mount above.
GATEWAY_DB: /app/gateway-data/workspace-gateway.db
PORT: "8402"
ports:
- "${WORKSPACE_GATEWAY_PORT:-8402}:8402"
volumes:
- workspace_gateway_data:/app/gateway-data
- ./logs:/app/logs
restart: unless-stopped
graph-gateway:
build: .
pull_policy: build
image: safi:latest
profiles: ["gateways"]
env_file:
- path: ./gateways/graph-gateway.env
required: false
environment:
SERVICE: graph-gateway
GATEWAY_DB: /app/gateway-data/graph-gateway.db
PORT: "8403"
ports:
- "${GRAPH_GATEWAY_PORT:-8403}:8403"
volumes:
- graph_gateway_data:/app/gateway-data
- ./logs:/app/logs
restart: unless-stopped
volumes:
db_data:
vector_store:
npm_cache:
# One per gateway, never shared: each holds a DIFFERENT provider's tokens
# under its own signing key, and the two sqlite files would otherwise sit in
# one directory for no reason.
workspace_gateway_data:
graph_gateway_data: