name: geological-rag x-runtime-config: &runtime-config APP_ENV: ${APP_ENV:-development} APP_NAME: ${APP_NAME:-geological-rag} POSTGRES_HOST: db POSTGRES_PORT: "5432" POSTGRES_DB: ${POSTGRES_DB:-geological_rag} POSTGRES_USER: ${POSTGRES_APP_USER:-geological_rag_app} POSTGRES_PASSWORD_FILE: /run/secrets/postgres_app_password UPLOAD_ROOT: ${UPLOAD_ROOT:-/data/uploads} MAX_UPLOAD_MB: "${MAX_UPLOAD_MB:-100}" DOCUMENT_NAMESPACE_MODE: ${DOCUMENT_NAMESPACE_MODE:-fake} x-rag-config: &rag-config DASHSCOPE_API_KEY_FILE: /run/secrets/bailian_api_key BAILIAN_OPENAI_BASE_URL: "${BAILIAN_OPENAI_BASE_URL:-}" BAILIAN_NATIVE_BASE_URL: "${BAILIAN_NATIVE_BASE_URL:-}" BAILIAN_RERANK_BASE_URL: "${BAILIAN_RERANK_BASE_URL:-}" EMBEDDING_MODEL: ${EMBEDDING_MODEL:-text-embedding-v4} EMBEDDING_DIMENSION: "${EMBEDDING_DIMENSION:-1024}" RERANK_MODEL: ${RERANK_MODEL:-qwen3-rerank} LLM_MODEL: ${LLM_MODEL:-deepseek-v4-flash} CHUNK_TARGET_TOKENS: "${CHUNK_TARGET_TOKENS:-512}" CHUNK_MAX_TOKENS: "${CHUNK_MAX_TOKENS:-800}" CHUNK_OVERLAP_TOKENS: "${CHUNK_OVERLAP_TOKENS:-64}" VECTOR_TOP_K: "${VECTOR_TOP_K:-50}" RERANK_TOP_N: "${RERANK_TOP_N:-10}" CONTEXT_TOP_N: "${CONTEXT_TOP_N:-8}" MAX_CONTEXT_TOKENS: "${MAX_CONTEXT_TOKENS:-10000}" MODEL_TIMEOUT_SECONDS: "${MODEL_TIMEOUT_SECONDS:-90}" MODEL_MAX_RETRIES: "${MODEL_MAX_RETRIES:-3}" MODEL_MAX_CONCURRENCY: "${MODEL_MAX_CONCURRENCY:-4}" x-model-client-config: &model-client-config MODEL_GATEWAY_BASE_URL: http://model-gateway:8000 MODEL_GATEWAY_TOKEN_FILE: /run/secrets/model_gateway_api_token MODEL_GATEWAY_CALLER: api MODEL_GATEWAY_TIMEOUT_SECONDS: "${MODEL_GATEWAY_TIMEOUT_SECONDS:-120}" services: db: image: pgvector/pgvector:0.8.2-pg17@sha256:feb68f4f15446397d8cac7f4fe48fe4586de83160d1fc48b46283312d1a33966 environment: POSTGRES_DB: ${POSTGRES_DB:-geological_rag} POSTGRES_USER: ${POSTGRES_BOOTSTRAP_USER:-postgres} POSTGRES_PASSWORD_FILE: /run/secrets/postgres_bootstrap_password POSTGRES_MIGRATOR_USER: ${POSTGRES_MIGRATOR_USER:-geological_rag_migrator} POSTGRES_MIGRATOR_PASSWORD_FILE: /run/secrets/postgres_migrator_password POSTGRES_APP_USER: ${POSTGRES_APP_USER:-geological_rag_app} POSTGRES_APP_PASSWORD_FILE: /run/secrets/postgres_app_password secrets: - postgres_bootstrap_password - postgres_migrator_password - postgres_app_password volumes: - postgres_data:/var/lib/postgresql/data - ./ops/postgres/init:/docker-entrypoint-initdb.d:ro healthcheck: test: - CMD-SHELL - >- pg_isready -h 127.0.0.1 -p 5432 -U $$POSTGRES_USER -d $$POSTGRES_DB && test -f $$PGDATA/.rag-bootstrap-complete interval: 5s timeout: 3s retries: 20 networks: - data restart: unless-stopped migrate: build: context: ./backend command: ["alembic", "upgrade", "head"] depends_on: db: condition: service_healthy environment: POSTGRES_HOST: db POSTGRES_PORT: "5432" POSTGRES_DB: ${POSTGRES_DB:-geological_rag} POSTGRES_USER: ${POSTGRES_MIGRATOR_USER:-geological_rag_migrator} POSTGRES_PASSWORD_FILE: /run/secrets/postgres_migrator_password secrets: - postgres_migrator_password networks: - data restart: "no" upload-init: build: context: ./backend command: ["python", "-m", "app.tools.init_upload_storage"] environment: UPLOAD_ROOT: /data/uploads user: "0:0" network_mode: none volumes: - uploads_data:/data/uploads read_only: true tmpfs: - /tmp security_opt: - no-new-privileges:true cap_drop: - ALL cap_add: - CHOWN - FOWNER - DAC_OVERRIDE restart: "no" api: build: context: ./backend command: ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] depends_on: db: condition: service_healthy migrate: condition: service_completed_successfully upload-init: condition: service_completed_successfully model-gateway: condition: service_healthy environment: <<: [*runtime-config, *model-client-config] secrets: - postgres_app_password - model_gateway_api_token volumes: - uploads_data:/data/uploads networks: - data - model healthcheck: test: - CMD - python - -c - >- import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/health/ready', timeout=2) interval: 10s timeout: 3s retries: 5 start_period: 5s init: true read_only: true tmpfs: - /tmp security_opt: - no-new-privileges:true cap_drop: - ALL restart: unless-stopped model-gateway: build: context: ./backend command: ["uvicorn", "app.model_gateway:app", "--host", "0.0.0.0", "--port", "8000"] environment: <<: *rag-config MODEL_GATEWAY_ALLOWED_TOKEN_FILES: >- api=/run/secrets/model_gateway_api_token,worker=/run/secrets/model_gateway_worker_token secrets: - bailian_api_key - model_gateway_api_token - model_gateway_worker_token networks: - model - egress healthcheck: test: - CMD - python - -c - >- import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/health/ready', timeout=2) interval: 10s timeout: 3s retries: 5 start_period: 5s init: true read_only: true tmpfs: - /tmp security_opt: - no-new-privileges:true cap_drop: - ALL restart: unless-stopped worker-local: build: context: ./backend command: ["python", "-m", "app.worker"] depends_on: db: condition: service_healthy migrate: condition: service_completed_successfully upload-init: condition: service_completed_successfully environment: <<: *runtime-config WORKER_CAPABILITIES: document_parse secrets: - postgres_app_password volumes: - uploads_data:/data/uploads networks: - data init: true read_only: true tmpfs: - /tmp security_opt: - no-new-privileges:true cap_drop: - ALL stop_grace_period: 150s restart: unless-stopped worker-model: build: context: ./backend command: ["python", "-m", "app.worker"] depends_on: db: condition: service_healthy migrate: condition: service_completed_successfully model-gateway: condition: service_healthy environment: <<: [*runtime-config, *model-client-config] WORKER_CAPABILITIES: embedding MODEL_GATEWAY_TOKEN_FILE: /run/secrets/model_gateway_worker_token MODEL_GATEWAY_CALLER: worker secrets: - postgres_app_password - model_gateway_worker_token networks: - data - model init: true read_only: true tmpfs: - /tmp security_opt: - no-new-privileges:true cap_drop: - ALL stop_grace_period: 150s restart: unless-stopped gateway: build: context: ./backend command: ["uvicorn", "app.gateway:app", "--host", "0.0.0.0", "--port", "8000"] depends_on: api: condition: service_healthy networks: - ingress - data healthcheck: test: - CMD - python - -c - >- import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/health/ready', timeout=2) interval: 10s timeout: 3s retries: 5 start_period: 5s init: true read_only: true tmpfs: - /tmp security_opt: - no-new-privileges:true cap_drop: - ALL restart: unless-stopped web: build: context: ./frontend depends_on: gateway: condition: service_healthy ports: - "127.0.0.1:8000:8080" networks: - edge - ingress healthcheck: test: - CMD - wget - --quiet - --output-document=- - http://127.0.0.1:8080/nginx-health interval: 10s timeout: 3s retries: 5 start_period: 5s init: true user: "101:101" read_only: true tmpfs: - /tmp:rw,noexec,nosuid,size=16m,uid=101,gid=101,mode=1770 security_opt: - no-new-privileges:true cap_drop: - ALL restart: unless-stopped provider-smoke: build: context: ./backend command: ["python", "-m", "app.tools.provider_smoke"] profiles: ["tools"] depends_on: model-gateway: condition: service_healthy environment: <<: *model-client-config EMBEDDING_MODEL: ${EMBEDDING_MODEL:-text-embedding-v4} EMBEDDING_DIMENSION: "${EMBEDDING_DIMENSION:-1024}" RERANK_MODEL: ${RERANK_MODEL:-qwen3-rerank} LLM_MODEL: ${LLM_MODEL:-deepseek-v4-flash} secrets: - model_gateway_api_token networks: - model restart: "no" seed-demo: build: context: ./backend command: ["python", "-m", "app.tools.seed_demo"] profiles: ["tools"] depends_on: migrate: condition: service_completed_successfully model-gateway: condition: service_healthy environment: <<: [*runtime-config, *rag-config, *model-client-config] DEMO_PROVIDER_MODE: bailian DEMO_DOCUMENTS_PATH: /demo/demo_documents.jsonl DEMO_QUERIES_PATH: /demo/demo_queries.jsonl MODEL_GATEWAY_TOKEN_FILE: /run/secrets/model_gateway_worker_token MODEL_GATEWAY_CALLER: worker secrets: - postgres_app_password - model_gateway_worker_token networks: - data - model volumes: - ./data/samples/public:/demo:ro restart: "no" seed-demo-offline: build: context: ./backend command: ["python", "-m", "app.tools.seed_demo"] profiles: ["tools"] depends_on: migrate: condition: service_completed_successfully environment: <<: *runtime-config DEMO_PROVIDER_MODE: fake DEMO_DOCUMENTS_PATH: /demo/demo_documents.jsonl DEMO_QUERIES_PATH: /demo/demo_queries.jsonl secrets: - postgres_app_password networks: - data volumes: - ./data/samples/public:/demo:ro restart: "no" worker-smoke: build: context: ./backend command: ["python", "-m", "app.tools.worker_smoke"] profiles: ["tools"] depends_on: migrate: condition: service_completed_successfully environment: <<: *runtime-config secrets: - postgres_app_password networks: - data init: true read_only: true tmpfs: - /tmp security_opt: - no-new-privileges:true cap_drop: - ALL restart: "no" document-pipeline-smoke: build: context: ./backend command: ["python", "-m", "app.tools.document_pipeline_smoke"] profiles: ["tools"] depends_on: gateway: condition: service_healthy worker-local: condition: service_started worker-model: condition: service_started environment: RAG_BASE_URL: http://gateway:8000 RAG_UPLOAD_SAMPLE: /demo/upload_demo.md DOCUMENT_NAMESPACE_MODE: ${DOCUMENT_NAMESPACE_MODE:-fake} volumes: - ./data/samples/public:/demo:ro networks: - ingress init: true read_only: true tmpfs: - /tmp security_opt: - no-new-privileges:true cap_drop: - ALL restart: "no" volumes: postgres_data: uploads_data: networks: edge: driver: bridge ingress: driver: bridge internal: true data: driver: bridge internal: true model: driver: bridge internal: true egress: driver: bridge secrets: postgres_bootstrap_password: file: ./secrets/postgres_bootstrap_password postgres_migrator_password: file: ./secrets/postgres_migrator_password postgres_app_password: file: ./secrets/postgres_app_password model_gateway_api_token: file: ./secrets/model_gateway_api_token model_gateway_worker_token: file: ./secrets/model_gateway_worker_token bailian_api_key: file: ./secrets/bailian_api_key