-
Notifications
You must be signed in to change notification settings - Fork 62
Expand file tree
/
Copy pathdocker-compose.yml
More file actions
332 lines (323 loc) · 13.2 KB
/
Copy pathdocker-compose.yml
File metadata and controls
332 lines (323 loc) · 13.2 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
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
x-logging: &default-logging
driver: json-file
options:
max-size: "10m"
max-file: "3"
services:
postgres:
image: pgvector/pgvector:pg15
container_name: fojin-postgres
restart: always
logging: *default-logging
# 1g chronically OOM-killed backends (155 cgroup OOMs in the dmesg
# buffer, all task=postgres) → full crash-recovery cycles dropping
# every connection. Raised to 3g; host has the headroom.
mem_limit: 4g
cpus: 1.0
# Docker's default /dev/shm is 64MB. Postgres puts parallel-worker shared
# memory there, so a parallel VACUUM / VACUUM ANALYZE on a large table
# (e.g. mitra_alignments ~800MB) fails with "could not resize shared memory
# segment ... No space left on device". 256MB is ample for maintenance and
# stays well under mem_limit (it's a lazily-allocated tmpfs). Autovacuum is
# single-process and unaffected either way. NOTE: applying a change here
# needs an explicit `docker compose up -d postgres` (brief DB blip) — the
# path-aware deploy.sh does not recreate postgres on a compose-only change.
shm_size: 256m
environment:
POSTGRES_DB: ${POSTGRES_DB:-fojin}
POSTGRES_USER: ${POSTGRES_USER:-fojin}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?Set POSTGRES_PASSWORD in .env}
ports:
- "127.0.0.1:${POSTGRES_PORT:-5432}:5432"
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-fojin}"]
interval: 5s
timeout: 3s
retries: 5
elasticsearch:
build:
context: ./elasticsearch
dockerfile: Dockerfile
container_name: fojin-es
restart: always
logging: *default-logging
mem_limit: 1536m
cpus: 1.5
environment:
- discovery.type=single-node
# Require auth. HTTP TLS stays off — ES is bound to 127.0.0.1 only, so
# basic auth over plaintext http on the loopback is the intended model.
- xpack.security.enabled=true
- xpack.security.http.ssl.enabled=false
- ES_JAVA_OPTS=-Xms512m -Xmx512m
ports:
- "127.0.0.1:${ES_PORT:-9200}:9200"
volumes:
- esdata:/usr/share/elasticsearch/data
healthcheck:
# Auth-agnostic: ES is up whether it answers 200 (no security) or 401
# (security on, no creds). A down node yields 000. Avoids embedding the
# password in the healthcheck.
test: ["CMD-SHELL", "c=$$(curl -s -o /dev/null -w '%{http_code}' http://localhost:9200/_cluster/health); [ \"$$c\" = 200 ] || [ \"$$c\" = 401 ]"]
interval: 10s
timeout: 5s
retries: 10
redis:
image: redis:7-alpine
container_name: fojin-redis
restart: always
logging: *default-logging
mem_limit: 256m
cpus: 0.5
# Require a password when REDIS_PASSWORD is set (empty → no auth, backward
# compatible). Read from the container env, not interpolated into argv.
environment:
REDIS_PASSWORD: ${REDIS_PASSWORD:-}
command: sh -c 'exec redis-server --requirepass "$$REDIS_PASSWORD"'
ports:
- "127.0.0.1:${REDIS_PORT:-6379}:6379"
volumes:
- redisdata:/data
healthcheck:
# Works with or without a password: auth ping first, else plain ping.
test: ["CMD-SHELL", "redis-cli -a \"$$REDIS_PASSWORD\" --no-auth-warning ping 2>/dev/null | grep -q PONG || redis-cli ping 2>/dev/null | grep -q PONG"]
interval: 5s
timeout: 3s
retries: 5
backend:
build:
context: ./backend
dockerfile: Dockerfile
container_name: fojin-backend
restart: always
logging: *default-logging
mem_limit: 1g
cpus: 1.0
environment:
POSTGRES_HOST: postgres
POSTGRES_PORT: 5432
POSTGRES_DB: ${POSTGRES_DB:-fojin}
POSTGRES_USER: ${POSTGRES_USER:-fojin}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?Set POSTGRES_PASSWORD in .env}
ES_HOST: http://elasticsearch:9200
REDIS_HOST: redis
REDIS_PORT: 6379
REDIS_PASSWORD: ${REDIS_PASSWORD:-}
ES_USER: ${ES_USER:-elastic}
ES_PASSWORD: ${ES_PASSWORD:-}
JWT_SECRET_KEY: ${JWT_SECRET_KEY:?Set JWT_SECRET_KEY in .env (32+ chars)}
# P0-1: BYOK encryption key, independent of JWT_SECRET_KEY.
API_KEY_ENCRYPTION_KEY: ${API_KEY_ENCRYPTION_KEY:?Set API_KEY_ENCRYPTION_KEY in .env (Fernet.generate_key() output)}
# Drives the production fail-fast block in app/config.py.
FOJIN_ENV: ${FOJIN_ENV:-production}
APP_VERSION: ${APP_VERSION:-}
FOJIN_COMMIT_SHA: ${FOJIN_COMMIT_SHA:-}
CORS_ORIGINS: ${CORS_ORIGINS:-http://localhost:3000,http://localhost:5173,http://localhost:5174}
LLM_API_URL: ${LLM_API_URL:-}
LLM_API_KEY: ${LLM_API_KEY:-}
LLM_MODEL: ${LLM_MODEL:-}
EMBEDDING_API_URL: ${EMBEDDING_API_URL:-}
EMBEDDING_API_KEY: ${EMBEDDING_API_KEY:-}
EMBEDDING_MODEL: ${EMBEDDING_MODEL:-BAAI/bge-m3}
# Cross-encoder reranker. When RERANKER_API_URL is set, rag_retrieval
# re-scores vector candidates with a cross-encoder (the single biggest
# RAG-precision lever); empty → falls back to keyword reranking.
# RERANKER_API_KEY is optional: rag_retrieval falls back to
# EMBEDDING_API_KEY (same SiliconFlow account hosts bge-reranker-v2-m3).
RERANKER_API_URL: ${RERANKER_API_URL:-}
RERANKER_API_KEY: ${RERANKER_API_KEY:-}
RERANKER_MODEL: ${RERANKER_MODEL:-BAAI/bge-reranker-v2-m3}
DIANJIN_API_KEY: ${DIANJIN_API_KEY:-}
AMAP_KEY: ${AMAP_KEY:-}
# OAuth & SMS
GITHUB_CLIENT_ID: ${GITHUB_CLIENT_ID:-}
GITHUB_CLIENT_SECRET: ${GITHUB_CLIENT_SECRET:-}
GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID:-}
GOOGLE_CLIENT_SECRET: ${GOOGLE_CLIENT_SECRET:-}
OAUTH_REDIRECT_BASE: ${OAUTH_REDIRECT_BASE:-http://localhost:3000}
ENABLE_SMS_LOGIN: ${ENABLE_SMS_LOGIN:-false}
ENABLE_OPEN_DATA_EXPORTS: ${ENABLE_OPEN_DATA_EXPORTS:-false}
ALIYUN_SMS_ACCESS_KEY_ID: ${ALIYUN_SMS_ACCESS_KEY_ID:-}
ALIYUN_SMS_ACCESS_KEY_SECRET: ${ALIYUN_SMS_ACCESS_KEY_SECRET:-}
ALIYUN_SMS_SIGN_NAME: ${ALIYUN_SMS_SIGN_NAME:-佛津}
ALIYUN_SMS_TEMPLATE_CODE: ${ALIYUN_SMS_TEMPLATE_CODE:-}
# 逐句对读 (sentence-level parallel) — ship-dark flag, reads from .env; default false.
ENABLE_SENTENCE_PARALLELS: ${ENABLE_SENTENCE_PARALLELS:-false}
ports:
- "127.0.0.1:${BACKEND_PORT:-8000}:8000"
volumes:
- ./backend:/app
- ./data:/data
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/api/health')"]
interval: 10s
timeout: 5s
retries: 10
start_period: 15s
depends_on:
postgres:
condition: service_healthy
elasticsearch:
condition: service_healthy
redis:
condition: service_healthy
# Second backend replica for zero-downtime rolling deploys. nginx upstream
# balances host 8000 + 8001; deploys recreate ONE replica at a time so the
# other keeps serving (no 502 window). Reuses the fojin-backend image — the
# app code is bind-mounted (./backend:/app), so both replicas run identical
# code. Start order matters: bring backend up (runs alembic) before backend2
# so the two never run `alembic upgrade head` concurrently.
backend2:
image: fojin-backend
container_name: fojin-backend2
restart: always
logging: *default-logging
mem_limit: 1g
cpus: 1.0
environment:
POSTGRES_HOST: postgres
POSTGRES_PORT: 5432
POSTGRES_DB: ${POSTGRES_DB:-fojin}
POSTGRES_USER: ${POSTGRES_USER:-fojin}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?Set POSTGRES_PASSWORD in .env}
ES_HOST: http://elasticsearch:9200
REDIS_HOST: redis
REDIS_PORT: 6379
REDIS_PASSWORD: ${REDIS_PASSWORD:-}
ES_USER: ${ES_USER:-elastic}
ES_PASSWORD: ${ES_PASSWORD:-}
JWT_SECRET_KEY: ${JWT_SECRET_KEY:?Set JWT_SECRET_KEY in .env (32+ chars)}
API_KEY_ENCRYPTION_KEY: ${API_KEY_ENCRYPTION_KEY:?Set API_KEY_ENCRYPTION_KEY in .env (Fernet.generate_key() output)}
FOJIN_ENV: ${FOJIN_ENV:-production}
APP_VERSION: ${APP_VERSION:-}
FOJIN_COMMIT_SHA: ${FOJIN_COMMIT_SHA:-}
CORS_ORIGINS: ${CORS_ORIGINS:-http://localhost:3000,http://localhost:5173,http://localhost:5174}
LLM_API_URL: ${LLM_API_URL:-}
LLM_API_KEY: ${LLM_API_KEY:-}
LLM_MODEL: ${LLM_MODEL:-}
EMBEDDING_API_URL: ${EMBEDDING_API_URL:-}
EMBEDDING_API_KEY: ${EMBEDDING_API_KEY:-}
EMBEDDING_MODEL: ${EMBEDDING_MODEL:-BAAI/bge-m3}
RERANKER_API_URL: ${RERANKER_API_URL:-}
RERANKER_API_KEY: ${RERANKER_API_KEY:-}
RERANKER_MODEL: ${RERANKER_MODEL:-BAAI/bge-reranker-v2-m3}
DIANJIN_API_KEY: ${DIANJIN_API_KEY:-}
AMAP_KEY: ${AMAP_KEY:-}
GITHUB_CLIENT_ID: ${GITHUB_CLIENT_ID:-}
GITHUB_CLIENT_SECRET: ${GITHUB_CLIENT_SECRET:-}
GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID:-}
GOOGLE_CLIENT_SECRET: ${GOOGLE_CLIENT_SECRET:-}
OAUTH_REDIRECT_BASE: ${OAUTH_REDIRECT_BASE:-http://localhost:3000}
ENABLE_SMS_LOGIN: ${ENABLE_SMS_LOGIN:-false}
ENABLE_OPEN_DATA_EXPORTS: ${ENABLE_OPEN_DATA_EXPORTS:-false}
ALIYUN_SMS_ACCESS_KEY_ID: ${ALIYUN_SMS_ACCESS_KEY_ID:-}
ALIYUN_SMS_ACCESS_KEY_SECRET: ${ALIYUN_SMS_ACCESS_KEY_SECRET:-}
ALIYUN_SMS_SIGN_NAME: ${ALIYUN_SMS_SIGN_NAME:-佛津}
ALIYUN_SMS_TEMPLATE_CODE: ${ALIYUN_SMS_TEMPLATE_CODE:-}
# 逐句对读 (sentence-level parallel) — ship-dark flag, reads from .env; default false.
ENABLE_SENTENCE_PARALLELS: ${ENABLE_SENTENCE_PARALLELS:-false}
ports:
- "127.0.0.1:8001:8000"
volumes:
- ./backend:/app
- ./data:/data
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/api/health')"]
interval: 10s
timeout: 5s
retries: 10
start_period: 15s
depends_on:
postgres:
condition: service_healthy
elasticsearch:
condition: service_healthy
redis:
condition: service_healthy
umami:
# Pinned by digest (v3.0.3) instead of the floating postgresql-latest tag,
# so a pull can't silently swap in unreviewed code or run a surprise
# schema migration. Bump deliberately.
image: ghcr.io/umami-software/umami:postgresql-latest@sha256:28f263fe06f79ebffa5a6a6e9bd33b7a278e9342a88e0bdac812416c9f9e4361
container_name: fojin-umami
restart: always
logging: *default-logging
mem_limit: 384m
cpus: 0.5
environment:
# Use a dedicated least-privilege role that can only reach the umami DB,
# so a compromise of this third-party container can't read/write the main
# fojin database (user table, BYOK ciphertexts). Falls back to the main
# postgres account when UMAMI_DB_* are unset (backward compatible).
DATABASE_URL: postgresql://${UMAMI_DB_USER:-${POSTGRES_USER:-fojin}}:${UMAMI_DB_PASSWORD:-${POSTGRES_PASSWORD}}@postgres:5432/umami
APP_SECRET: ${UMAMI_APP_SECRET:?Set UMAMI_APP_SECRET in .env}
BASE_PATH: /umami
ports:
- "127.0.0.1:3001:3000"
depends_on:
postgres:
condition: service_healthy
healthcheck:
test: ["CMD-SHELL", "wget -q --spider http://127.0.0.1:3000/api/heartbeat || exit 1"]
interval: 10s
timeout: 5s
retries: 5
frontend:
depends_on:
backend:
condition: service_healthy
build:
context: ./frontend
dockerfile: Dockerfile
args:
VITE_UMAMI_URL: ${VITE_UMAMI_URL:-}
VITE_UMAMI_WEBSITE_ID: ${VITE_UMAMI_WEBSITE_ID:-}
container_name: fojin-frontend
restart: always
logging: *default-logging
mem_limit: 128m
cpus: 0.25
ports:
# Bind to loopback by default, like every other service here. Docker
# publishes ports via its own iptables rules that bypass host firewalls
# (e.g. ufw), so "3000:80" would expose the container — and the /api and
# /umami proxies behind it — on the host's public interface, defeating a
# CDN/WAF and leaking the origin IP. A host reverse proxy or CDN should be
# the public entry. Self-hosters wanting direct LAN/remote access without
# a proxy can set FRONTEND_BIND=0.0.0.0.
- "${FRONTEND_BIND:-127.0.0.1}:${FRONTEND_PORT:-3000}:80"
# Hosted MCP endpoint (mcp.fojin.ai) — fojin's corpus tools for AI agents.
# Talks to the backend directly over the compose network (no nginx hop):
# internal traffic carries no X-Forwarded-For, which is what exempts it from
# the backend's per-IP limiter (app/core/rate_limit.py) — this container
# runs its own per-client window keyed on CF-Connecting-IP instead.
mcp:
build:
context: ./mcp-server
dockerfile: Dockerfile
container_name: fojin-mcp
restart: always
logging: *default-logging
mem_limit: 192m
cpus: 0.25
environment:
FOJIN_API_BASE_URL: http://backend:8000/api
FOJIN_MCP_PUBLIC_HOSTS: ${FOJIN_MCP_PUBLIC_HOSTS:-mcp.fojin.ai}
FOJIN_MCP_RATE_LIMIT: ${FOJIN_MCP_RATE_LIMIT:-30}
ports:
- "127.0.0.1:${MCP_PORT:-8765}:8765"
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8765/healthz')"]
interval: 30s
timeout: 5s
retries: 5
start_period: 10s
depends_on:
backend:
condition: service_healthy
volumes:
pgdata:
esdata:
redisdata: