Files
mcp-nextcloud/docker-compose.yml
T

403 lines
16 KiB
YAML

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:11.8.8-noble@sha256:be1ef4fe5f14589325c08a41c76334097ce66c86264b75e1d28342c742782a61
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:09160599abd229764c0fb44cb6be640294e1d360a54b19985ab4843dcf2d90f1
restart: always
app:
image: ${NEXTCLOUD_IMAGE:-docker.io/library/nextcloud:32.0.11@sha256:3ae4045d53e890680ec35e3e4349332123d959c72808aceb44930e79f1be9b9b}
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.1-alpine@sha256:8b1e78743a03dbb2c95171cc58639fef29abc8816598e27fb910ed2e621e589a
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
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
- VECTOR_SYNC_SCAN_INTERVAL=5
- VECTOR_SYNC_PROCESSOR_WORKERS=2
#- 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)
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
# 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 credentials for background sync (optional - uses DCR if not provided)
# Uncomment to avoid DCR:
# - 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.3@sha256:5fdbf2dbb5897cc34e82de49d13e23db011f9925089dbc555fc095f2c8bc1dac
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
# 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
# ADR-005: Token exchange mode (RFC 8693)
# Exchange MCP tokens (aud: nextcloud-mcp-server) for Nextcloud tokens (aud: http://localhost:8080)
# Provides strict audience separation between MCP session and Nextcloud API access
- ENABLE_TOKEN_EXCHANGE=true
- TOKEN_EXCHANGE_CACHE_TTL=300 # Cache exchanged tokens for 5 minutes (default)
# 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
# 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
# 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.1-alpine@sha256:8b1e78743a03dbb2c95171cc58639fef29abc8816598e27fb910ed2e621e589a
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:16-alpine@sha256:16bc17c64a573ef34162af9298258d1aec548232985b33ed7b1eac33ba35c229
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
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: