Remove the oauth Docker Compose profile (mcp-oauth service, port 8001) which used OAuth bearer tokens for direct NC API access, requiring upstream OIDC patches. All NC access should use app passwords via Login Flow v2 or BasicAuth. Changes: - Remove mcp-oauth service from docker-compose.yml - Remove oauth mode from CI test matrix - Delete oauth pass-through tests (core, permissions, token exchange) - Delete oauth-specific tests (elicitation, NC PHP app, astrolabe) - Migrate MCP/OAuth integration tests to login-flow profile: - DCR lifecycle, deletion, token type tests - Scope authorization (tool filtering) tests - Token introspection tests - Fix flaky consent screen automation: replace JS btn.click() with Playwright native click + retry (handles Vue.js event binding race) - Add scope-filtered OAuth client fixtures to login-flow conftest - Keep keycloak profile for external IdP testing Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
342 lines
13 KiB
YAML
342 lines
13 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.6-noble@sha256:31c1aa20ba2d78c4cbc45b2675e40ffd6a3cd59897a9767ce88fc284309f55ab
|
|
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.6.2-alpine@sha256:81b6f81d6a6c5b9019231a2e8eb10085e3a139a34f833dcc965a8a959b040b72
|
|
restart: always
|
|
|
|
app:
|
|
image: ${NEXTCLOUD_IMAGE:-docker.io/library/nextcloud:32.0.6@sha256:5c4e09f72f096cd68379a8ae69f71e61d13da5a07430fc4a17c702a14e6a4267}
|
|
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.29.7-alpine@sha256:e7257f1ef28ba17cf7c248cb8ccf6f0c6e0228ab9c315c152f9c203cd34cf6d1
|
|
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.1@sha256:f81233ad3049c0b81e010b9be123ed64698787d3218f8356f3e8756f4c14de2f
|
|
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
|
|
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=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)
|
|
- 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
|
|
|
|
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
|
|
# --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)
|
|
- 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
|
|
|
|
# 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.94.2@sha256:95e528798bebe75f39b10e74e7051cf51188ee615934f232ba7ad06a3390ffa1
|
|
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.29.7-alpine@sha256:e7257f1ef28ba17cf7c248cb8ccf6f0c6e0228ab9c315c152f9c203cd34cf6d1
|
|
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.17.1@sha256:94728574965d17c6485dd361aa3c0818b325b9016dac5ea6afec7b4b2700865f
|
|
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:
|
|
keycloak-tokens:
|
|
keycloak-oauth-storage:
|
|
login-flow-data:
|
|
login-flow-oauth-storage:
|
|
qdrant-data:
|
|
mcp-data:
|
|
multi-user-basic-data:
|
|
tailscale-state:
|