name: zp-agent services: zp-agent-api: build: context: .. dockerfile: deployment/Dockerfile args: EMBEDDING_MODEL: ${EMBEDDING_MODEL:-intfloat/multilingual-e5-small} EMBEDDING_REVISION: ${EMBEDDING_REVISION:-} CHUNK_TOKEN_ENCODING: ${CHUNK_TOKEN_ENCODING:-cl100k_base} RAG_LLM_TOKEN_ENCODING: ${RAG_LLM_TOKEN_ENCODING:-cl100k_base} container_name: zp-agent-api ports: - "8000:8000" env_file: - ../.env environment: ZPWIKI_ROOT: /zpwiki WIKI_SYNC_INTERVAL_SECONDS: ${WIKI_SYNC_INTERVAL_SECONDS:-86400} SYNC_UPSTREAM_FORK: ${SYNC_UPSTREAM_FORK:-1} ZPWIKI_SYNC_KEY_FILE: /run/secrets/zpwiki_ssh_key CHUNK_MAX_TOKENS: "450" CHUNK_OVERLAP_TOKENS: "70" CHUNK_MIN_TOKENS: "80" CHUNK_TOKEN_ENCODING: ${CHUNK_TOKEN_ENCODING:-cl100k_base} RAG_LLM_MAX_TOKENS: ${RAG_LLM_MAX_TOKENS:-12000} RAG_LLM_TOKEN_ENCODING: ${RAG_LLM_TOKEN_ENCODING:-cl100k_base} HF_HOME: /cache/huggingface HF_HUB_OFFLINE: "1" TRANSFORMERS_OFFLINE: "1" ZP_RUNTIME_ASSETS: /opt/zp-assets ZP_ASSETS_REQUIRED: "1" GIT_SSH_COMMAND: >- ssh -F /dev/null -i /run/secrets/zpwiki_ssh_key -o IdentitiesOnly=yes -o BatchMode=yes -o StrictHostKeyChecking=yes -o UserKnownHostsFile=/run/secrets/zpwiki_known_hosts -o ConnectTimeout=15 NEO4J_URI: bolt://neo4j:7687 NEO4J_USER: ${NEO4J_USER:-neo4j} NEO4J_PASSWORD: ${NEO4J_PASSWORD} NEO4J_DATABASE: ${NEO4J_DATABASE:-neo4j} volumes: - ../data:/app/data - ../../zpwiki:/zpwiki - hf-cache:/cache/huggingface - type: bind source: ${ZPWIKI_SSH_KEY:-${HOME}/.ssh/id_ed25519} target: /run/secrets/zpwiki_ssh_key read_only: true bind: create_host_path: false - type: bind source: ${ZPWIKI_KNOWN_HOSTS:-${HOME}/.ssh/known_hosts} target: /run/secrets/zpwiki_known_hosts read_only: true bind: create_host_path: false restart: unless-stopped stop_grace_period: 5m depends_on: neo4j: condition: service_healthy healthcheck: test: ["CMD", "python", "-m", "tools.runtime.runtime_health"] interval: 15s timeout: 10s retries: 12 start_period: 90s claude-mcp: profiles: [claude] build: context: .. dockerfile: deployment/claude.Dockerfile ports: - "127.0.0.1:8001:8001" environment: ZP_AGENT_URL: http://zp-agent-api:8000 ZP_AGENT_API_KEY: ${SEARCH_API_KEY} ZP_AGENT_ALLOW_HTTP: "1" ZP_MCP_ACCESS_TOKEN: ${ZP_MCP_ACCESS_TOKEN:-} ZP_MCP_PUBLIC_URL: ${ZP_MCP_PUBLIC_URL:-} ZP_MCP_BIND: "0.0.0.0" ZP_MCP_AUTH_MODE: ${ZP_MCP_AUTH_MODE:-oauth} ZP_MCP_LOGIN_PASSWORD: ${ZP_MCP_LOGIN_PASSWORD:-} volumes: - claude-oauth:/var/lib/zp-oauth depends_on: zp-agent-api: condition: service_healthy healthcheck: test: - CMD - python - -c - >- import os,urllib.request; r=urllib.request.Request('http://127.0.0.1:8001/health',headers={'Authorization':'Bearer '+os.environ['ZP_MCP_ACCESS_TOKEN']}); assert urllib.request.urlopen(r,timeout=5).status==200 interval: 15s timeout: 6s retries: 5 restart: unless-stopped read_only: true cap_drop: [ALL] security_opt: [no-new-privileges:true] # The tunnel connects through Docker DNS, not a possibly different WSL # localhost service. OAuth on claude-mcp still protects every tool call. claude-tunnel: profiles: [claude-tunnel] image: cloudflare/cloudflared@sha256:4955524aeaa379e154023d5a027c250f7b35041a2e7e950cf13f144bc9b9c5ba command: [tunnel, --no-autoupdate, --protocol, http2, --url, "http://claude-mcp:8001"] depends_on: claude-mcp: condition: service_healthy restart: unless-stopped read_only: true cap_drop: [ALL] security_opt: [no-new-privileges:true] neo4j: image: neo4j:2026.09.0 container_name: zp-agent-neo4j ports: - "127.0.0.1:7474:7474" - "127.0.0.1:7687:7687" environment: NEO4J_AUTH: ${NEO4J_USER:-neo4j}/${NEO4J_PASSWORD} volumes: - neo4j-data:/data - neo4j-logs:/logs healthcheck: test: [ "CMD-SHELL", "u=$${NEO4J_AUTH%%/*}; p=$${NEO4J_AUTH#*/}; cypher-shell -u \"$$u\" -p \"$$p\" 'RETURN 1' >/dev/null 2>&1" ] interval: 10s timeout: 5s retries: 10 start_period: 20s restart: unless-stopped volumes: claude-oauth: hf-cache: neo4j-data: neo4j-logs: