forked from cbcoutinho/nextcloud-mcp-server
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdocker-compose.yml
More file actions
544 lines (503 loc) · 23.4 KB
/
Copy pathdocker-compose.yml
File metadata and controls
544 lines (503 loc) · 23.4 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
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
services:
# Note: MariaDB is external service. You can find more information about the configuration here:
# https://hub.docker.com/_/mariadb
db:
# Note: Check the recommend version here: https://docs.nextcloud.com/server/latest/admin_manual/installation/system_requirements.html#server
image: docker.io/library/mariadb:12.3.2-noble@sha256:628f228f0fd5913a220438693576b29b6fe4dc1fa0a1298c0e98579fae28635f
restart: always
command: --transaction-isolation=READ-COMMITTED
volumes:
- db:/var/lib/mysql
ports:
- 127.0.0.1:3306:3306
environment:
- MYSQL_ROOT_PASSWORD=password
- MYSQL_PASSWORD=password
- MYSQL_DATABASE=nextcloud
- MYSQL_USER=nextcloud
# Note: Redis is an external service. You can find more information about the configuration here:
# https://hub.docker.com/_/redis
redis:
image: docker.io/library/redis:8.8.0-alpine@sha256:9d317178eceac8454a2284a9e6df2466b93c745529947f0cd42a0fa9609d7005
restart: always
app:
image: ${NEXTCLOUD_IMAGE:-docker.io/library/nextcloud:32.0.12@sha256:41fe07a7397cd8e4a6b5873bcea83ba29d40e3333351c7aabc591f9ee1b0ef42}
restart: always
ports:
- 127.0.0.1:8080:80
depends_on:
- redis
- db
volumes:
- nextcloud:/var/www/html
- ./app-hooks:/docker-entrypoint-hooks.d:ro
# Mount OIDC development directory outside /var/www/html to avoid rsync conflicts
# The post-installation hook will register /opt/apps as an additional app directory
#- ./third_party:/opt/apps:ro
#- ./third_party/astrolabe:/opt/apps/astrolabe:ro
#- ./third_party/oidc:/opt/apps/oidc:ro
environment:
- NEXTCLOUD_TRUSTED_DOMAINS=app
- NEXTCLOUD_ADMIN_USER=admin
- NEXTCLOUD_ADMIN_PASSWORD=admin
- MYSQL_PASSWORD=password
- MYSQL_DATABASE=nextcloud
- MYSQL_USER=nextcloud
- MYSQL_HOST=db
- REDIS_HOST=redis
# Set MCP_SERVER_URL to enable Astrolabe OAuth client auto-creation
# (before-starting hook creates OIDC client + stores credentials in config.php)
# Example: MCP_SERVER_URL=http://mcp-login-flow:8004 docker compose --profile login-flow up -d
- MCP_SERVER_URL=${MCP_SERVER_URL:-}
healthcheck:
test: ["CMD-SHELL", "curl -Ss http://localhost/status.php | grep '\"installed\":true' || exit 1"]
interval: 10s
timeout: 30s
retries: 30
recipes:
image: docker.io/library/nginx:1.31.2-alpine@sha256:54f2a904c251d5a34adf545a72d32515a15e08418dae0266e23be2e18c66fefa
restart: always
volumes:
- ./tests/fixtures/test_recipe.html:/usr/share/nginx/html/test_recipe.html:ro
- ./tests/fixtures/nginx.conf:/etc/nginx/nginx.conf:ro
unstructured:
image: downloads.unstructured.io/unstructured-io/unstructured-api:0.1.7@sha256:ee89a87a545b6a7d56609607aced71b5b1acf0211400bf2d11a8f70f209a93bd
restart: always
ports:
- 127.0.0.1:8005:8000
# Unstructured API runs on port 8000 internally
# We expose it on 8005 externally to avoid conflict
profiles:
- unstructured
# docling-serve: OCR-strong document parsing (photographed / scanned /
# handwritten text). Opt-in via the "docling" profile. Point the MCP server at
# it with ENABLE_DOCLING=true + DOCLING_API_URL=http://docling:5001 (and/or
# DOCUMENT_OCR_PROVIDER=docling for scanned-PDF OCR). Uses the CPU-only image
# variant (docling-serve-cpu) — no CUDA runtime, OCR models baked in — so it
# runs on plain GitHub-hosted runners. CPU OCR is slow; give it a generous
# DOCLING_TIMEOUT.
docling:
image: ghcr.io/docling-project/docling-serve-cpu:v1.26.0@sha256:7e07522e0240c1db3ff5b837ffa969c2ecd5a71664c0e0369f5a69fc169e30ba
restart: always
ports:
- 127.0.0.1:5001:5001
# docling-serve is a Python app (no curl in the image), so probe /health via
# the stdlib. Gives local `--profile docling` users a readiness signal and
# lets a future `depends_on: condition: service_healthy` gate on it.
healthcheck:
test:
- CMD
- python3
- -c
- import urllib.request; urllib.request.urlopen("http://localhost:5001/health").read()
interval: 10s
timeout: 10s
retries: 30
start_period: 30s
profiles:
- docling
mcp:
build: .
restart: always
command: ["--transport", "streamable-http"]
depends_on:
app:
condition: service_healthy
ports:
- 127.0.0.1:8000:8000
- 127.0.0.1:9090:9090
volumes:
- mcp-data:/app/data
- ./settings.toml.example:/app/settings.toml:ro
environment:
- NEXTCLOUD_HOST=http://app:80
- NEXTCLOUD_USERNAME=admin
- NEXTCLOUD_PASSWORD=admin
- NEXTCLOUD_PUBLIC_ISSUER_URL=http://localhost:8080
# Pin the token DB inside the mcp-data volume so the runtime default
# (ephemeral tempfile) doesn't silently apply inside containers.
- TOKEN_STORAGE_DB=/app/data/tokens.db
# Semantic search configuration (ADR-007, ADR-021)
- ENABLE_SEMANTIC_SEARCH=true
# Search mode (ADR-030): hybrid (default, dense+sparse) | keyword (BM25
# sparse only). Set keyword for fully airgapped deployments with NO
# embedding endpoint (no Ollama/Bedrock/gateway) — still gives cross-app
# Qdrant full-text search. keyword still needs ENABLE_SEMANTIC_SEARCH=true.
#- SEARCH_MODE=keyword
# The sync scanner runs an initial scan immediately on startup, so the
# corpus is indexed without waiting for the first periodic re-scan. A very
# short re-scan interval (was 5s) just re-queues the whole corpus faster
# than the workers can drain it: on a loaded CI runner pending_count
# snowballs (observed 1465 pending), sync never reaches idle, and the
# single-user vector tests (test_rag, test_sampling) time out. 30s matches
# the tuned multi-user-basic cadence and stays well within the tests' 90s
# sync-wait budget.
- VECTOR_SYNC_SCAN_INTERVAL=30
- VECTOR_SYNC_PROCESSOR_WORKERS=2
# Required to enable the /webhooks/nextcloud receiver (GHSA-8vh3-g2qg-2h2c).
# Without it the route is not mounted and vector sync relies on polling.
- WEBHOOK_SECRET=${WEBHOOK_SECRET:-dev-webhook-secret-change-me}
#- LOG_FORMAT=json
# Qdrant configuration (three modes):
# 1. Network mode: Set QDRANT_URL=http://qdrant:6333 (requires qdrant service)
# 2. In-memory mode: Set QDRANT_LOCATION=:memory: (default if nothing set)
# 3. Persistent local: Set QDRANT_LOCATION=/app/data/qdrant (stored in mcp-data volume)
- QDRANT_LOCATION=":memory:"
#- QDRANT_URL=http://qdrant:6333 # Uncomment for network mode
#- QDRANT_API_KEY=${QDRANT_API_KEY:-my_secret_api_key} # Only for network mode
# Observability
#- OTEL_SERVICE_NAME=nextcloud-mcp-docker-compose
#- OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317
# Collection naming: Auto-generated as {deployment-id}-{model-name}
# - Deployment ID: OTEL_SERVICE_NAME (if set) or hostname (fallback)
# - Model name: OLLAMA_EMBEDDING_MODEL
# - Example: "nextcloud-mcp-server-nomic-embed-text"
# - Changing models creates new collection (requires re-embedding)
# - Set QDRANT_COLLECTION to override auto-generation:
#- QDRANT_COLLECTION=nextcloud_content
# Ollama configuration (optional - uses SimpleEmbeddingProvider if not set)
# - OLLAMA_BASE_URL=http://ollama:11434
# - OLLAMA_EMBEDDING_MODEL=nomic-embed-text # Changing this creates new collection
# - OLLAMA_VERIFY_SSL=false
# Document chunking configuration (for vector embeddings)
# Tune these based on your embedding model and content type
# - DOCUMENT_CHUNK_SIZE=512 # Words per chunk (default: 512)
# - DOCUMENT_CHUNK_OVERLAP=50 # Overlapping words (default: 50, recommended: 10-20% of chunk size)
# Docling OCR-strong parsing backend (docling-serve HTTP API, ADR-031).
# Off by default so the normal single-user lane is unchanged. The CI
# "docling" lane starts the `docling` profile and sets
# ENABLE_DOCUMENT_PROCESSING=true + ENABLE_DOCLING=true +
# DOCLING_API_URL=http://docling:5001 to exercise the gated
# tests/integration/test_docling_api.py suite. DOCLING_TIMEOUT is bumped in
# CI because CPU OCR is slow. ENABLE_DOCUMENT_PROCESSING is the master
# switch: initialize_document_processors() no-ops without it, so docling
# never registers on the webdav read_file path if it is unset.
- ENABLE_DOCUMENT_PROCESSING=${ENABLE_DOCUMENT_PROCESSING:-false}
- ENABLE_DOCLING=${ENABLE_DOCLING:-false}
- DOCLING_API_URL=${DOCLING_API_URL:-}
- DOCLING_TIMEOUT=${DOCLING_TIMEOUT:-120}
profiles:
- single-user
mcp-multi-user-basic:
build: .
restart: always
command: ["--transport", "streamable-http"]
depends_on:
app:
condition: service_healthy
ports:
- 127.0.0.1:8003:8000
environment:
# Multi-user BasicAuth pass-through mode (ADR-020, ADR-022)
- NEXTCLOUD_HOST=http://app:80
- NEXTCLOUD_MCP_SERVER_URL=http://localhost:8003
- NEXTCLOUD_PUBLIC_ISSUER_URL=http://localhost:8080
- MCP_DEPLOYMENT_MODE=multi_user_basic
- ENABLE_BACKGROUND_OPERATIONS=true
# Token storage (required for middleware initialization).
# Source the key from .env — see env.sample. To generate a fresh key:
# python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
- TOKEN_ENCRYPTION_KEY=${TOKEN_ENCRYPTION_KEY:?TOKEN_ENCRYPTION_KEY must be set in .env (see env.sample)}
- TOKEN_STORAGE_DB=/app/data/tokens.db
- ENABLE_SEMANTIC_SEARCH=true
# Required to enable the /webhooks/nextcloud receiver (GHSA-8vh3-g2qg-2h2c).
# Without it the route is not mounted and vector sync relies on polling.
- WEBHOOK_SECRET=${WEBHOOK_SECRET:-dev-webhook-secret-change-me}
# Tuned cadence for the multi-user background-sync integration suite,
# which provisions a user, creates a note, then waits ~90s for it to be
# indexed. Two independent knobs matter here:
# * USER_POLL_INTERVAL gates how fast the background-sync user manager
# *discovers* a freshly-provisioned user (its scanner runs an initial
# scan immediately on start). The 60s default left too little of the
# 90s budget, so drop it to 5s for prompt discovery.
# * SCAN_INTERVAL gates *re-scan* churn. Each scan re-queues the user's
# whole corpus, so a very short interval (e.g. 5s) floods the single
# processor worker faster than it drains on slower CI runners
# (pending climbs, indexed stays 0). Keep it moderate: the immediate
# initial scan already indexes the corpus once; 30s re-scans avoid
# the flood while still catching a note created just after that scan.
- VECTOR_SYNC_USER_POLL_INTERVAL=5
- VECTOR_SYNC_SCAN_INTERVAL=30
- VECTOR_SYNC_PROCESSOR_WORKERS=1
# OAuth client for the MCP server's IdP registration. Falls back to DCR
# if unset — but with Nextcloud's built-in `oidc` app, DCR clients expire
# after ~1h (client_expire_time) and break auth permanently (see #907).
# For any non-CI deployment, register a static client and set these:
# - NEXTCLOUD_OIDC_CLIENT_ID=your_client_id
# - NEXTCLOUD_OIDC_CLIENT_SECRET=your_client_secret
# Management API allowlist (ADR-018): only Astrolabe-issued tokens may
# call /api/management/*. Default test client_id used by the
# configure_astrolabe_for_mcp_server fixture.
- ALLOWED_MGMT_CLIENT=nextcloudMcpServerUIPublicClient
# NO admin credentials - credentials come from client Authorization header
volumes:
- multi-user-basic-data:/app/data
- ./settings.toml.example:/app/settings.toml:ro
profiles:
- multi-user-basic
keycloak:
image: quay.io/keycloak/keycloak:26.6.4@sha256:0aae0de7fca85525f727d3354df17896092de8bb26ae4c12d89c77e5df8cbce4
command:
- "start-dev"
- "--import-realm"
- "--hostname=http://localhost:8888"
- "--hostname-strict=false"
- "--hostname-backchannel-dynamic=true"
- "--features=preview" # Enable Legacy V1 token exchange (supports both Standard V2 and Legacy V1)
ports:
- 127.0.0.1:8888:8080
environment:
- KC_BOOTSTRAP_ADMIN_USERNAME=admin
- KC_BOOTSTRAP_ADMIN_PASSWORD=admin
volumes:
- ./keycloak/realm-export.json:/opt/keycloak/data/import/realm.json:ro
healthcheck:
test: ["CMD-SHELL", "exec 3<>/dev/tcp/localhost/8080 && echo -e 'GET /realms/nextcloud-mcp HTTP/1.1\\r\\nHost: localhost\\r\\nConnection: close\\r\\n\\r\\n' >&3 && cat <&3 | grep -q 'HTTP/1.1 200'"]
interval: 10s
timeout: 5s
retries: 30
profiles:
- keycloak
mcp-keycloak:
build: .
command: ["--transport", "streamable-http", "--oauth", "--port", "8002"]
restart: always
depends_on:
keycloak:
condition: service_healthy
app:
condition: service_started
ports:
- 127.0.0.1:8002:8002
environment:
# Generic OIDC configuration (external IdP mode - Keycloak)
# Provider auto-detected from OIDC_DISCOVERY_URL issuer
# Using internal Docker hostname for discovery to get consistent issuer
- OIDC_DISCOVERY_URL=http://keycloak:8080/realms/nextcloud-mcp/.well-known/openid-configuration
- NEXTCLOUD_OIDC_CLIENT_ID=nextcloud-mcp-server
- NEXTCLOUD_OIDC_CLIENT_SECRET=mcp-secret-change-in-production
- OIDC_JWKS_URI=http://keycloak:8080/realms/nextcloud-mcp/protocol/openid-connect/certs
# Nextcloud API endpoint (for accessing APIs with validated token)
- NEXTCLOUD_HOST=http://app:80
- NEXTCLOUD_MCP_SERVER_URL=http://localhost:8002
- NEXTCLOUD_RESOURCE_URI=nextcloud # ADR-005: Keycloak uses client IDs as audiences, not URLs
- NEXTCLOUD_PUBLIC_ISSUER_URL=http://localhost:8888/realms/nextcloud-mcp
# External-IdP mode: the OAuth issuer (above) is Keycloak, but Login Flow
# v2 must send the browser to *Nextcloud*. NEXTCLOUD_HOST is the internal
# Docker hostname, so give the browser-reachable Nextcloud URL explicitly.
- NEXTCLOUD_PUBLIC_URL=http://localhost:8080
# Refresh token storage (ADR-002 Tier 1 & 2). Source from .env.
- ENABLE_BACKGROUND_OPERATIONS=true
- TOKEN_ENCRYPTION_KEY=${TOKEN_ENCRYPTION_KEY:?TOKEN_ENCRYPTION_KEY must be set in .env (see env.sample)}
- TOKEN_STORAGE_DB=/app/data/tokens.db
# Login Flow v2 (ADR-022) with external IdP — derived from the
# auto-detected LOGIN_FLOW deployment mode; no separate flag needed.
- ENABLE_DCR=true
# OAuth scopes (optional - uses defaults if not specified)
- NEXTCLOUD_OIDC_SCOPES=openid profile email offline_access notes.read notes.write calendar.read calendar.write contacts.read contacts.write cookbook.read cookbook.write deck.read deck.write tables.read tables.write files.read files.write sharing.read sharing.write todo.read todo.write
# NO admin credentials - using external IdP OAuth only!
volumes:
- keycloak-tokens:/app/data
- keycloak-oauth-storage:/app/.oauth
- ./settings.toml.example:/app/settings.toml:ro
profiles:
- keycloak
# Login Flow v2 mode (ADR-022)
# Test with: docker compose --profile login-flow up --build -d
mcp-login-flow:
build: .
restart: always
# --oauth enables the OAuth/OIDC identity layer that Login Flow v2 builds on
# (user identity via OAuth session, Nextcloud access via app passwords)
command: ["--transport", "streamable-http", "--oauth", "--port", "8004"]
depends_on:
app:
condition: service_healthy
ports:
- 127.0.0.1:8004:8004
environment:
- NEXTCLOUD_HOST=http://app:80
- NEXTCLOUD_MCP_SERVER_URL=http://localhost:8004
#- NEXTCLOUD_MCP_SERVER_URL=https://nextcloud-mcp-dev.tail148d5.ts.net
- NEXTCLOUD_PUBLIC_ISSUER_URL=http://localhost:8080
# Login Flow v2 (ADR-022) — the deployment mode is the single switch;
# the browser-based app-password layer is derived automatically.
- MCP_DEPLOYMENT_MODE=login_flow
# NOTE: this dev/test service relies on Dynamic Client Registration for
# the MCP server's own OIDC client, which is fine for short-lived CI runs.
# For a real self-hosted login_flow deployment, register a STATIC client
# in Nextcloud (Administration → OpenID Connect provider) and set
# NEXTCLOUD_OIDC_CLIENT_ID / NEXTCLOUD_OIDC_CLIENT_SECRET — the built-in
# `oidc` app deletes DCR clients after ~1h, which breaks auth (see #907
# and docs/login-flow-v2.md → Troubleshooting).
# Token storage (required for app password + session persistence).
# Source the key from .env — see env.sample. To generate a fresh key:
# python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
- TOKEN_ENCRYPTION_KEY=${TOKEN_ENCRYPTION_KEY:?TOKEN_ENCRYPTION_KEY must be set in .env (see env.sample)}
- TOKEN_STORAGE_DB=/app/data/tokens.db
# Semantic search
- ENABLE_SEMANTIC_SEARCH=true
- VECTOR_SYNC_SCAN_INTERVAL=60
- VECTOR_SYNC_PROCESSOR_WORKERS=1
# Required to enable the /webhooks/nextcloud receiver (GHSA-8vh3-g2qg-2h2c).
# Without it the route is not mounted and vector sync relies on polling.
- WEBHOOK_SECRET=${WEBHOOK_SECRET:-dev-webhook-secret-change-me}
# Management API allowlist (ADR-018) — matches mcp-multi-user-basic so
# the same configure_astrolabe_for_mcp_server fixture (which creates the
# static `nextcloudMcpServerUIPublicClient` OIDC client) works for tests
# against this profile too.
- ALLOWED_MGMT_CLIENT=astrolabeMcpClientOAuth00000000000
volumes:
- login-flow-data:/app/data
- login-flow-oauth-storage:/app/.oauth
- ./settings.toml.example:/app/settings.toml:ro
profiles:
- login-flow
# Tailscale Funnel for Claude AI connector testing
# Usage: docker compose --profile login-flow --profile claude-funnel up --build -d
# Requires: TS_AUTHKEY in .env file (see env.sample.claude-funnel)
tailscale-mcp:
image: docker.io/tailscale/tailscale:v1.98.4@sha256:25cde9ad76020b0e29229136d0c38b5962e9a0e1774ffac9b0df68e4a37d6cf0
hostname: nextcloud-mcp-dev
restart: always
environment:
- TS_AUTHKEY=${TS_AUTHKEY}
- TS_STATE_DIR=/var/lib/tailscale
- TS_SERVE_CONFIG=/config/serve-config.json
- TS_HOSTNAME=nextcloud-mcp-dev
- TS_EXTRA_ARGS=--advertise-tags=tag:container
volumes:
- tailscale-state:/var/lib/tailscale
- ./claude-funnel/serve-config.json:/config/serve-config.json:ro
cap_add:
- NET_ADMIN
- SYS_MODULE
profiles:
- claude-funnel
nginx-claude-filter:
image: docker.io/library/nginx:1.31.2-alpine@sha256:54f2a904c251d5a34adf545a72d32515a15e08418dae0266e23be2e18c66fefa
restart: always
depends_on:
- mcp-login-flow
volumes:
- ./claude-funnel/nginx.conf:/etc/nginx/nginx.conf:ro
profiles:
- claude-funnel
qdrant:
image: docker.io/qdrant/qdrant:v1.18.2@sha256:75eab8c4ba42096724fdcfde8b4de0b5713d529dde32f285a1f86fdcb2c9e50c
restart: always
ports:
- 127.0.0.1:6333:6333 # REST API
- 127.0.0.1:6334:6334 # gRPC (optional)
volumes:
- qdrant-data:/qdrant/storage
environment:
- QDRANT__SERVICE__API_KEY=${QDRANT_API_KEY:-my_secret_api_key}
healthcheck:
test: ["CMD-SHELL", "test -f /qdrant/.qdrant-initialized"]
interval: 10s
timeout: 5s
retries: 10
profiles:
- qdrant
# Centralized Postgres backend for token storage. Used by:
# 1. Integration tests gated on @pytest.mark.postgres — they read
# TEST_DATABASE_URL=postgresql+asyncpg://mcp:mcp@localhost:5433/mcp.
# 2. Manual smoke testing of the HA (multi-replica) deployment story.
# Bring up with: docker compose --profile postgres up -d postgres-test
# See ADR-026 for the pluggable-database-backend design.
postgres-test:
image: docker.io/library/postgres:18-alpine@sha256:1b1689b20d16a014a3d195653381cf2caa75a41a92d93b255a9d6ea29fd353aa
restart: unless-stopped
environment:
POSTGRES_USER: mcp
POSTGRES_PASSWORD: mcp
POSTGRES_DB: mcp
ports:
- 127.0.0.1:5433:5432
healthcheck:
test: ["CMD-SHELL", "pg_isready -U mcp -d mcp"]
interval: 5s
timeout: 3s
retries: 10
profiles:
- postgres
# GreenMail: standalone SMTP/IMAP test server for exercising the Nextcloud
# Mail app end-to-end (#965 follow-up). `greenmail.auth.disabled` makes any
# login succeed and auto-creates the mailbox, so the Nextcloud Mail account
# provisioned by the post-installation hook can connect without seeding users.
# SMTP 3025 / IMAP 3143 are the plaintext ports (ssl-mode "none").
greenmail:
image: docker.io/greenmail/standalone:2.1.9@sha256:3ac5a83dd6727cf95e4d50e18907fb8ee7bbf5f67e8534714dee2fb1b5b2e1d4
restart: always
environment:
- GREENMAIL_OPTS=-Dgreenmail.setup.test.all -Dgreenmail.hostname=0.0.0.0 -Dgreenmail.auth.disabled -Dgreenmail.verbose
ports:
# Plaintext SMTP/IMAP + the management REST API (8080 → 8085 to avoid the
# host 8080 already taken by the `app` service).
- 127.0.0.1:3025:3025
- 127.0.0.1:3143:3143
- 127.0.0.1:8085:8080
healthcheck:
# The slim JRE base has no curl/wget, but the Ubuntu base ships bash, so
# probe the IMAP port directly (open once `setup.test.all` has bound it).
test: ["CMD", "bash", "-c", "exec 3<>/dev/tcp/127.0.0.1/3143"]
interval: 5s
timeout: 5s
retries: 20
profiles:
- mail
# OpenLDAP backend for the `ldap` lane — reproduces GH #980 (DAV paths built
# from loginName instead of the canonical Nextcloud UID). Nextcloud's
# user_ldap backend derives an internal UID from the LDAP UUID that differs
# from the login (`alice`), which single-user / user_oidc / login-by-email
# backends can't produce. Bootstrapped fresh on every `up` (no data volume),
# so the custom LDIF is always applied.
openldap:
image: docker.io/vegardit/openldap:2.6.10@sha256:a635242fe7bdafd8c59dbd20ffd43c77d6e3b930903d1c4580b6974b2e0e67ba
environment:
- LDAP_INIT_ORG_DN=dc=example,dc=org
- LDAP_INIT_ORG_NAME=Example Org
- LDAP_INIT_ROOT_USER_PW=ldap_admin_pw
# Plain LDAP on 389 is enough for the internal test network; skip the
# self-signed LDAPS setup so user_ldap can connect without TLS fuss.
- LDAP_LDAPS_ENABLED=false
volumes:
# Seed ou=people + the divergent `alice` user (evaluated on first launch;
# no data volume, so it re-seeds fresh on every `up`).
- ./ldap/bootstrap.ldif:/opt/ldifs/init_org_entries.ldif:ro
ports:
- 127.0.0.1:1389:389
# Searching for the seeded `alice` entry (not just a port probe) means
# "healthy" == "bootstrap LDIF applied" — the app-hook can rely on that.
healthcheck:
test:
[
"CMD",
"ldapsearch",
"-x",
"-H",
"ldap://localhost",
"-b",
"uid=alice,ou=people,dc=example,dc=org",
"-D",
"uid=admin,dc=example,dc=org",
"-w",
"ldap_admin_pw",
]
interval: 10s
timeout: 5s
retries: 30
profiles:
- ldap
volumes:
nextcloud:
db:
keycloak-tokens:
keycloak-oauth-storage:
login-flow-data:
login-flow-oauth-storage:
qdrant-data:
mcp-data:
multi-user-basic-data:
tailscale-state: