db1e0606ad
Consolidate three independent RefreshTokenStorage lazy singletons into a single lock-protected get_shared_storage() function, eliminating race conditions on concurrent first-access. Remove blanket try/except in _get_stored_scopes so storage errors propagate as proper MCP errors instead of silently triggering "please provision" messages. Handle declined/cancelled elicitation results in Login Flow tools by cleaning up sessions and returning clear status. Add update_app_password_scopes() to avoid unnecessary decrypt/re-encrypt when only scopes change. Add unprovisioned-user early exit and no-op detection to nc_auth_update_scopes. Remove four dead config fields and misleading NEXTCLOUD_PASSWORD deprecation warning. Add periodic login flow session cleanup task. Generate separate Fernet keys per service. Add board cleanup in deck integration test. Gate CI unit tests on linting and skip Astrolabe build for single-user profile. Fix test markers from oauth to multi_user_basic for astrolabe integration tests. Update login_flow.py docstrings to document outbound HTTP calls. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
375 lines
14 KiB
YAML
375 lines
14 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:lts@sha256:8164f184d16c30e2f159e30518113667b796306dff0fe558876ab1ff521a682f
|
|
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:alpine@sha256:2afba59292f25f5d1af200496db41bea2c6c816b059f57ae74703a50a03a27d0
|
|
restart: always
|
|
|
|
app:
|
|
image: docker.io/library/nextcloud:32.0.6@sha256:dcf9c6019d05df721bb7bada99748964c95446ea479771e9073ceaded733407e
|
|
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 # Use app store version; dev mount lacks vendor/
|
|
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
|
|
- 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:alpine@sha256:5878d06ae4c83d73285438255f705bb3f9a736f41cd24876ed25bb33faf76c7d
|
|
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:latest@sha256:ba6cb073af079c498e9466a5a9152ba4b6c9cad12efeeaf053ba383023d5db08
|
|
restart: always
|
|
ports:
|
|
- 127.0.0.1:8002:8000
|
|
# Unstructured API runs on port 8000 internally
|
|
# We expose it on 8002 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
|
|
environment:
|
|
- NEXTCLOUD_HOST=http://app:80
|
|
- NEXTCLOUD_USERNAME=admin
|
|
- NEXTCLOUD_PASSWORD=admin
|
|
- NEXTCLOUD_PUBLIC_ISSUER_URL=http://localhost:8080
|
|
|
|
# Semantic search configuration (ADR-007, ADR-021)
|
|
#- ENABLE_SEMANTIC_SEARCH=true
|
|
- VECTOR_SYNC_SCAN_INTERVAL=60
|
|
- VECTOR_SYNC_PROCESSOR_WORKERS=1
|
|
|
|
#- 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=/app/data/qdrant # In-memory mode used if not set
|
|
#- 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)
|
|
- NEXTCLOUD_HOST=http://app:80
|
|
- NEXTCLOUD_MCP_SERVER_URL=http://localhost:8003
|
|
- NEXTCLOUD_PUBLIC_ISSUER_URL=http://localhost:8080
|
|
- ENABLE_MULTI_USER_BASIC_AUTH=true
|
|
- ENABLE_BACKGROUND_OPERATIONS=true
|
|
|
|
# Token storage (required for middleware initialization)
|
|
# DEVELOPMENT ONLY - generate a fresh key for production:
|
|
# python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
|
|
- TOKEN_ENCRYPTION_KEY=fqqI4G51yBCOcu9cvv6wCUJB7sf_CK2za5ClC6b86yY=
|
|
- TOKEN_STORAGE_DB=/app/data/tokens.db
|
|
|
|
- ENABLE_SEMANTIC_SEARCH=true
|
|
- VECTOR_SYNC_SCAN_INTERVAL=60
|
|
- 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
|
|
|
|
# NO admin credentials - credentials come from client Authorization header
|
|
volumes:
|
|
- multi-user-basic-data:/app/data
|
|
profiles:
|
|
- multi-user-basic
|
|
|
|
mcp-oauth:
|
|
build: .
|
|
command: ["--transport", "streamable-http", "--oauth", "--port", "8001", "--oauth-token-type", "jwt"]
|
|
restart: always
|
|
depends_on:
|
|
app:
|
|
condition: service_healthy
|
|
ports:
|
|
- 127.0.0.1:8001:8001
|
|
environment:
|
|
# Generic OIDC configuration (integrated mode - Nextcloud OIDC app)
|
|
# OIDC_DISCOVERY_URL not set - defaults to NEXTCLOUD_HOST/.well-known/openid-configuration
|
|
# OIDC_CLIENT_ID not set - uses Dynamic Client Registration (DCR)
|
|
- NEXTCLOUD_HOST=http://app:80
|
|
- NEXTCLOUD_MCP_SERVER_URL=http://localhost:8001
|
|
- NEXTCLOUD_RESOURCE_URI=http://localhost:8080 # ADR-005: Nextcloud resource identifier for audience validation
|
|
- NEXTCLOUD_PUBLIC_ISSUER_URL=http://localhost:8080
|
|
- NEXTCLOUD_OIDC_SCOPES=openid profile email 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
|
|
|
|
# Refresh token storage (ADR-002 Tier 1)
|
|
- ENABLE_BACKGROUND_OPERATIONS=true
|
|
- TOKEN_ENCRYPTION_KEY=Qh60VwZQsM7CLtSMunzC0gIGPBT948S6VSawUkODtvU=
|
|
- TOKEN_STORAGE_DB=/app/data/tokens.db
|
|
|
|
# ADR-005: Multi-audience mode (default - ENABLE_TOKEN_EXCHANGE=false)
|
|
# Tokens must contain BOTH MCP and Nextcloud audiences
|
|
# No token exchange needed - tokens work for both MCP auth and Nextcloud APIs
|
|
|
|
# Semantic search configuration (ADR-007, ADR-021)
|
|
- ENABLE_SEMANTIC_SEARCH=true
|
|
- VECTOR_SYNC_SCAN_INTERVAL=60
|
|
- VECTOR_SYNC_PROCESSOR_WORKERS=1
|
|
|
|
# Qdrant configuration - persistent local storage
|
|
- QDRANT_LOCATION=/app/data/qdrant
|
|
|
|
# Embedding provider for vector sync (use Simple provider as fallback)
|
|
# Ollama not available in CI/test environments
|
|
# - OLLAMA_BASE_URL=http://ollama:11434
|
|
# - OLLAMA_EMBEDDING_MODEL=nomic-embed-text
|
|
|
|
# NO admin credentials - using OAuth with Dynamic Client Registration (DCR)
|
|
# Client credentials registered via RFC 7591 and stored in volume
|
|
# JWT token type is used for testing (faster validation, scopes embedded in token)
|
|
volumes:
|
|
- oauth-client-storage:/app/.oauth
|
|
- oauth-tokens:/app/data
|
|
profiles:
|
|
- oauth
|
|
|
|
keycloak:
|
|
image: quay.io/keycloak/keycloak:26.5.4@sha256:ae8efb0d218d8921334b03a2dbee7069a0b868240691c50a3ffc9f42fabba8b4
|
|
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)
|
|
- ENABLE_BACKGROUND_OPERATIONS=true
|
|
- TOKEN_ENCRYPTION_KEY=ESF1BvEQdGYsCluwMx9Cxvw3uh5pFowPH7Rg_nIliyo=
|
|
- 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)
|
|
|
|
# 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
|
|
profiles:
|
|
- keycloak
|
|
|
|
# Login Flow v2 mode (ADR-022)
|
|
# Test with: docker compose --profile login-flow up --build -d
|
|
mcp-login-flow:
|
|
build: .
|
|
restart: always
|
|
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_PUBLIC_ISSUER_URL=http://localhost:8080
|
|
|
|
# Login Flow v2 (ADR-022)
|
|
- ENABLE_LOGIN_FLOW=true
|
|
|
|
# Token storage (required for app password + session persistence)
|
|
# DEVELOPMENT ONLY - generate a fresh key for production:
|
|
# python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
|
|
- TOKEN_ENCRYPTION_KEY=rxJvkBf7ZBjZZDL4a1sSqjhmjawhmbRMSOGfK8HDyKU=
|
|
- TOKEN_STORAGE_DB=/app/data/tokens.db
|
|
|
|
# Semantic search
|
|
- ENABLE_SEMANTIC_SEARCH=true
|
|
- VECTOR_SYNC_SCAN_INTERVAL=60
|
|
- VECTOR_SYNC_PROCESSOR_WORKERS=1
|
|
volumes:
|
|
- login-flow-data:/app/data
|
|
- login-flow-oauth-storage:/app/.oauth
|
|
profiles:
|
|
- login-flow
|
|
|
|
# Smithery stateless deployment mode (ADR-016)
|
|
# Test with: docker compose --profile smithery up smithery
|
|
# Then: curl http://localhost:8081/.well-known/mcp-config
|
|
smithery:
|
|
build:
|
|
context: .
|
|
dockerfile: Dockerfile.smithery
|
|
restart: always
|
|
depends_on:
|
|
app:
|
|
condition: service_healthy
|
|
ports:
|
|
- 127.0.0.1:8081:8081
|
|
environment:
|
|
- SMITHERY_DEPLOYMENT=true
|
|
- ENABLE_SEMANTIC_SEARCH=false
|
|
- PORT=8081
|
|
profiles:
|
|
- smithery
|
|
|
|
qdrant:
|
|
image: docker.io/qdrant/qdrant:v1.17.0@sha256:f1c7272cdac52b38c1a0e89313922d940ba50afd90d593a1605dbbc214e66ffb
|
|
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
|
|
|
|
volumes:
|
|
nextcloud:
|
|
db:
|
|
oauth-client-storage:
|
|
oauth-tokens:
|
|
keycloak-tokens:
|
|
keycloak-oauth-storage:
|
|
login-flow-data:
|
|
login-flow-oauth-storage:
|
|
qdrant-data:
|
|
mcp-data:
|
|
multi-user-basic-data:
|