volumes: appsmith_data: caddy-config: caddy-data: comfyui_data: docling_cache: flowise: grafana: keelflow_data: langfuse_clickhouse_data: langfuse_clickhouse_logs: langfuse_minio_data: postgres_data: name: localai_langfuse_postgres_data letta_data: libretranslate_api_keys: libretranslate_models: lightrag_data: lightrag_inputs: n8n_sandbox_api_data: n8n_sandbox_tls: n8n_storage: nocodb_data: ollama_storage: open-webui: open_terminal_home: openclaw_data: openclaw_ssh: paddle_cache: paddleocr_cache: paddlex_data: portainer_data: databasus_data: postiz-config: postiz-uploads: prometheus_data: qdrant_storage: ragflow_elasticsearch_data: ragflow_minio_data: ragflow_mysql_data: ragflow_redis_data: temporal_elasticsearch_data: valkey-data: uptime_kuma_data: weaviate_data: # Private link between Caddy and the OpenClaw gateway. Only caddy, openclaw and # openclaw-init join it: OpenClaw trusts the identity header only from this # subnet (openclaw/init.sh), so containers on the default network cannot # impersonate the user. The host holds the subnet's gateway address and is # trusted too, so any host process could - accepted for single-admin servers, # see openclaw/init.sh. networks: openclaw-proxy: # Shared logging configuration for services x-logging: &default-logging driver: "json-file" options: max-size: "1m" max-file: "1" # Shared proxy configuration for services that need outbound proxy support x-proxy-env: &proxy-env HTTP_PROXY: "${GOST_PROXY_URL:-}" HTTPS_PROXY: "${GOST_PROXY_URL:-}" http_proxy: "${GOST_PROXY_URL:-}" https_proxy: "${GOST_PROXY_URL:-}" NO_PROXY: "${GOST_NO_PROXY:-}" no_proxy: "${GOST_NO_PROXY:-}" x-n8n: &service-n8n build: context: ./n8n dockerfile: Dockerfile.n8n pull: true environment: &service-n8n-env <<: *proxy-env DB_POSTGRESDB_DATABASE: postgres DB_POSTGRESDB_HOST: postgres DB_POSTGRESDB_PASSWORD: "${POSTGRES_PASSWORD}" DB_POSTGRESDB_USER: postgres DB_TYPE: postgresdb EXECUTIONS_MODE: "${EXECUTIONS_MODE:-queue}" GENERIC_TIMEZONE: "${GENERIC_TIMEZONE:-America/New_York}" LANGCHAIN_API_KEY: "${LANGCHAIN_API_KEY}" LANGCHAIN_ENDPOINT: "${LANGCHAIN_ENDPOINT}" LANGCHAIN_TRACING_V2: "${LANGCHAIN_TRACING_V2}" N8N_BINARY_DATA_MODE: database N8N_BLOCK_ENV_ACCESS_IN_NODE: false N8N_BLOCK_FILE_ACCESS_TO_N8N_FILES: "${N8N_BLOCK_FILE_ACCESS_TO_N8N_FILES:-true}" N8N_COMMUNITY_PACKAGES_ALLOW_TOOL_USAGE: true N8N_DIAGNOSTICS_ENABLED: false N8N_EMAIL_MODE: "${N8N_EMAIL_MODE:-smtp}" # Empty = n8n defaults (instance-ai is on by default). Set "instance-ai,agents" # for the Agents preview. An unknown module name stops n8n from booting. N8N_ENABLED_MODULES: "${N8N_ENABLED_MODULES:-}" N8N_ENCRYPTION_KEY: "${N8N_ENCRYPTION_KEY}" N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS: true # n8n Assistant sandbox (n8n-sandbox profile). ENABLED is written by # 05_configure_services.sh; the URL and key point at the sandbox-api service. N8N_INSTANCE_AI_SANDBOX_ENABLED: "${N8N_INSTANCE_AI_SANDBOX_ENABLED:-false}" N8N_INSTANCE_AI_SEARXNG_URL: "${N8N_INSTANCE_AI_SEARXNG_URL:-}" N8N_LOG_LEVEL: "${N8N_LOG_LEVEL:-info}" N8N_LOG_OUTPUT: "${N8N_LOG_OUTPUT:-console}" N8N_METRICS: true N8N_METRICS_INCLUDE_MESSAGE_EVENT_BUS_METRICS: true N8N_METRICS_INCLUDE_WORKFLOW_ID_LABEL: true N8N_METRICS_INCLUDE_WORKFLOW_NAME_LABEL: true N8N_METRICS_INCLUDE_WORKFLOW_INFO: true N8N_PAYLOAD_SIZE_MAX: "${N8N_PAYLOAD_SIZE_MAX:-256}" N8N_PERSONALIZATION_ENABLED: false N8N_PROXY_HOPS: "${N8N_PROXY_HOPS:-1}" N8N_RESTRICT_FILE_ACCESS_TO: /data/shared N8N_RUNNERS_AUTH_TOKEN: "${N8N_RUNNERS_AUTH_TOKEN}" N8N_RUNNERS_BROKER_LISTEN_ADDRESS: 0.0.0.0 N8N_RUNNERS_ENABLED: true N8N_RUNNERS_MODE: external N8N_RUNNERS_TASK_REQUEST_TIMEOUT: "${N8N_RUNNERS_TASK_REQUEST_TIMEOUT:-300}" N8N_SANDBOX_SERVICE_API_KEY: "${N8N_SANDBOX_API_KEY:-}" N8N_SANDBOX_SERVICE_URL: "${N8N_SANDBOX_SERVICE_URL:-http://sandbox-api:8080}" OFFLOAD_MANUAL_EXECUTIONS_TO_WORKERS: "${OFFLOAD_MANUAL_EXECUTIONS_TO_WORKERS:-true}" N8N_SMTP_HOST: "${N8N_SMTP_HOST:-}" N8N_SMTP_OAUTH_PRIVATE_KEY: "${N8N_SMTP_OAUTH_PRIVATE_KEY:-}" N8N_SMTP_OAUTH_SERVICE_CLIENT: "${N8N_SMTP_OAUTH_SERVICE_CLIENT:-}" N8N_SMTP_PASS: "${N8N_SMTP_PASS:-}" N8N_SMTP_PORT: "${N8N_SMTP_PORT:-}" N8N_SMTP_SENDER: "${N8N_SMTP_SENDER:-}" N8N_SMTP_SSL: "${N8N_SMTP_SSL:-true}" N8N_SMTP_STARTTLS: "${N8N_SMTP_STARTTLS:-true}" N8N_SMTP_USER: "${N8N_SMTP_USER:-}" N8N_USER_MANAGEMENT_JWT_SECRET: "${N8N_USER_MANAGEMENT_JWT_SECRET}" NODE_ENV: production NODES_EXCLUDE: "${NODES_EXCLUDE:-[]}" QUEUE_BULL_PREFIX: "${QUEUE_BULL_PREFIX:-n8n}" QUEUE_BULL_REDIS_HOST: "${REDIS_HOST:-redis}" QUEUE_BULL_REDIS_PORT: "${REDIS_PORT:-6379}" QUEUE_HEALTH_CHECK_ACTIVE: true WEBHOOK_URL: "${N8N_HOSTNAME:+https://}${N8N_HOSTNAME:-http://localhost:5678}/" x-ollama: &service-ollama image: ollama/ollama:latest container_name: ollama restart: unless-stopped environment: OLLAMA_CONTEXT_LENGTH: "${OLLAMA_CONTEXT_LENGTH:-8192}" OLLAMA_FLASH_ATTENTION: 1 OLLAMA_GPU_OVERHEAD: "${OLLAMA_GPU_OVERHEAD:-0}" OLLAMA_KEEP_ALIVE: "${OLLAMA_KEEP_ALIVE:-}" OLLAMA_KV_CACHE_TYPE: "${OLLAMA_KV_CACHE_TYPE:-q8_0}" OLLAMA_MAX_LOADED_MODELS: "${OLLAMA_MAX_LOADED_MODELS:-2}" OLLAMA_NUM_PARALLEL: "${OLLAMA_NUM_PARALLEL:-}" OLLAMA_SCHED_SPREAD: "${OLLAMA_SCHED_SPREAD:-}" # Optional extra environment for every instance (llama.cpp LLAMA_ARG_*, # ROCm HSA_OVERRIDE_GFX_VERSION, ...). Kept out of 'environment:' on purpose: # an interpolated "${VAR:-}" would hand llama-server an EMPTY value, and an # empty LLAMA_ARG_FIT_TARGET makes it exit at startup. A missing file adds # nothing. Keys set in 'environment:' above always win over this file. # Per-instance files (ollama2.env, ...) are added by generate_ollama_instances.sh. env_file: - path: ./ollama.env required: false volumes: - ollama_storage:/root/.ollama x-init-ollama: &init-ollama image: ollama/ollama:latest container_name: ollama-pull-llama volumes: - ollama_storage:/root/.ollama entrypoint: /bin/sh command: - "-c" - "sleep 3; OLLAMA_HOST=ollama:11434 ollama pull qwen2.5:7b-instruct-q4_K_M; OLLAMA_HOST=ollama:11434 ollama pull nomic-embed-text" # InvokeAI shared config (hardware variants below, mutually exclusive profiles like Ollama) x-invokeai: &service-invokeai container_name: invokeai restart: unless-stopped logging: *default-logging environment: &invokeai-env <<: *proxy-env volumes: - ./invokeai:/invokeai healthcheck: test: ["CMD-SHELL", "http_proxy= https_proxy= HTTP_PROXY= HTTPS_PROXY= python3 -c \"import urllib.request; urllib.request.urlopen('http://localhost:9090/api/v1/app/version', timeout=5)\" || exit 1"] interval: 30s timeout: 10s retries: 5 start_period: 90s # ComfyUI shared config (hardware variants below, mutually exclusive profiles like InvokeAI). # The volume MUST be /root: the image runs as root, its entrypoint copies the # bundled ComfyUI to /root/ComfyUI on first start and models, custom nodes and # user data live under it. (/home/runner, the old mount point, is the non-root # layout of the archived cu121 image; every tag this stack has used runs as # root, so nothing was ever written there.) Consequence: the ComfyUI code itself lives # in the volume and is updated through ComfyUI-Manager, not by pulling the image. x-comfyui: &service-comfyui container_name: comfyui restart: unless-stopped logging: *default-logging volumes: - comfyui_data:/root healthcheck: test: ["CMD", "python3", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8188', timeout=5)"] interval: 10s timeout: 5s retries: 5 start_period: 60s # Worker-runner anchor for sidecar pattern (runner connects to worker via localhost) x-n8n-worker-runner: &service-n8n-worker-runner build: context: ./n8n dockerfile: Dockerfile.runner pull: true environment: <<: *proxy-env GENERIC_TIMEZONE: "${GENERIC_TIMEZONE:-America/New_York}" N8N_RUNNERS_AUTH_TOKEN: "${N8N_RUNNERS_AUTH_TOKEN}" N8N_RUNNERS_AUTO_SHUTDOWN_TIMEOUT: "${N8N_RUNNERS_AUTO_SHUTDOWN_TIMEOUT:-0}" N8N_RUNNERS_MAX_CONCURRENCY: "${N8N_RUNNERS_MAX_CONCURRENCY:-10}" N8N_RUNNERS_TASK_BROKER_URI: http://127.0.0.1:5679 services: appsmith: image: appsmith/appsmith-ce:release container_name: appsmith profiles: ["appsmith"] restart: unless-stopped logging: *default-logging environment: <<: *proxy-env APPSMITH_ENCRYPTION_PASSWORD: "${APPSMITH_ENCRYPTION_PASSWORD}" APPSMITH_ENCRYPTION_SALT: "${APPSMITH_ENCRYPTION_SALT}" APPSMITH_DISABLE_TELEMETRY: "true" volumes: - appsmith_data:/appsmith-stacks healthcheck: test: ["CMD-SHELL", "http_proxy= https_proxy= HTTP_PROXY= HTTPS_PROXY= curl -fsS http://localhost/api/v1/health || exit 1"] interval: 30s timeout: 10s retries: 5 start_period: 120s flowise: # Pinned: 3.1.4 crashes at startup with SQLite ("this.db.exec is not a # function") and runs as 'node', ignoring the /root/.flowise mount. # Keep in sync with scripts/update_preview.sh. image: flowiseai/flowise:3.1.3 restart: unless-stopped container_name: flowise profiles: ["flowise"] environment: <<: *proxy-env PORT: 3001 extra_hosts: - "host.docker.internal:host-gateway" volumes: - ~/.flowise:/root/.flowise entrypoint: /bin/sh -c "sleep 3; flowise start" keelflow: image: ghcr.io/perruer/keelflow:latest restart: unless-stopped container_name: keelflow profiles: ["keelflow"] logging: *default-logging environment: <<: *proxy-env KEELFLOW_HOME: /home/node/.keelflow extra_hosts: - "host.docker.internal:host-gateway" volumes: - keelflow_data:/home/node/.keelflow healthcheck: test: ["CMD-SHELL", "http_proxy= https_proxy= HTTP_PROXY= HTTPS_PROXY= curl -fsS http://localhost:3000/api/v1/ping || exit 1"] interval: 30s timeout: 10s retries: 5 start_period: 60s # --------------------------------------------------------------------------- # Open Terminal (profile "open-terminal") - execution environment for Open WebUI # agents: shell, filesystem, runtime package installs. Internal only: no ports, # no Caddy block. Open WebUI reaches it as http://open-terminal:8000; the admin # adds it under Admin Settings > Integrations > Open Terminal, so the key stays # server-side. Only the full image ("latest" or a release tag) supports # multi-user and runtime installs; the slim/alpine/openshift variants do # neither, and 05_configure_services.sh refuses them in multi-user mode. # Multi-user creates a plain Linux # account (no sudo) per Open WebUI user id, hence the volume is /home, not # /home/user; with multi-user off the chat gets the image's sudo-capable user. # Never mount docker.sock here: the entrypoint would hand the agent's shell the # host Docker daemon. OPEN_TERMINAL_ALLOWED_DOMAINS is deliberately absent: the # entrypoint reads a set-but-empty value as "block all egress" and needs # NET_ADMIN for iptables, so an interpolated default would crash the container. # Add it together with cap_add NET_ADMIN in docker-compose.override.yml if wanted. # --------------------------------------------------------------------------- open-terminal: image: "ghcr.io/open-webui/open-terminal:${OPEN_TERMINAL_VERSION:-latest}" container_name: open-terminal profiles: ["open-terminal"] restart: unless-stopped logging: *default-logging environment: <<: *proxy-env OPEN_TERMINAL_API_KEY: "${OPEN_TERMINAL_API_KEY}" OPEN_TERMINAL_MULTI_USER: "${OPEN_TERMINAL_MULTI_USER:-true}" # Reinstalled on every container start. In multi-user mode this is the only # way to add apt packages (per-user accounts have no sudo). OPEN_TERMINAL_PACKAGES: "${OPEN_TERMINAL_PACKAGES:-}" OPEN_TERMINAL_PIP_PACKAGES: "${OPEN_TERMINAL_PIP_PACKAGES:-}" OPEN_TERMINAL_NPM_PACKAGES: "${OPEN_TERMINAL_NPM_PACKAGES:-}" volumes: - open_terminal_home:/home # Safety valve against a runaway build or a stuck port proxy, not a quota. deploy: resources: limits: cpus: "${OPEN_TERMINAL_CPU_LIMIT:-2.0}" memory: "${OPEN_TERMINAL_MEMORY_LIMIT:-2G}" healthcheck: test: ["CMD-SHELL", "http_proxy= https_proxy= HTTP_PROXY= HTTPS_PROXY= curl -fs http://localhost:8000/docs || exit 1"] interval: 30s timeout: 10s retries: 5 start_period: 60s open-webui: image: ghcr.io/open-webui/open-webui:main restart: unless-stopped container_name: open-webui profiles: ["open-webui"] environment: <<: *proxy-env OLLAMA_BASE_URL: http://ollama:11434 # Without it the key lives in the container layer and changes on every recreate WEBUI_SECRET_KEY: "${WEBUI_SECRET_KEY:-}" extra_hosts: - "host.docker.internal:host-gateway" volumes: - open-webui:/app/backend/data healthcheck: test: ["CMD-SHELL", "http_proxy= https_proxy= HTTP_PROXY= HTTPS_PROXY= curl -fs http://localhost:8080/health || exit 1"] interval: 30s timeout: 10s retries: 5 start_period: 120s # --------------------------------------------------------------------------- # OpenClaw (profile "openclaw") - personal AI agent with the Control UI and a # Telegram bot. The dashboard sits behind Caddy basic auth only: the gateway runs in # trusted-proxy mode, Caddy passes the basic-auth user in X-Forwarded-User over # the private openclaw-proxy network, and browsers are auto-approved, so there # is no second password and no device pairing screen. openclaw-init (see # openclaw/init.sh) copies the SSH key and enforces the gateway keys on every # 'compose up'. OPENCLAW_GATEWAY_PASSWORD is only for the local CLI # ('make openclaw a="..."') and init.sh's password-login fallback. # FULL HOST ACCESS BY DESIGN: docker.sock (root-equivalent) plus an SSH key # that 05_configure_services.sh authorizes for the installing user # ('ssh host' inside the container). Anyone who passes the OpenClaw basic # auth or is approved in Telegram controls the server; keep the Telegram DM # policy on pairing or an allowlist. # --------------------------------------------------------------------------- openclaw-init: build: &openclaw-build context: ./openclaw pull: true args: OPENCLAW_VERSION: "${OPENCLAW_VERSION:-latest}" image: localai-openclaw container_name: openclaw-init profiles: ["openclaw"] restart: "no" logging: *default-logging user: "0:0" entrypoint: ["sh", "/openclaw-init.sh"] # Same values as the gateway: 'config set' validates the whole config, # including the ${OPENCLAW_HOSTNAME} / ${OPENCLAW_USERNAME} references and, # on the password-login fallback, the gateway password. # TELEGRAM_BOT_TOKEN decides whether init.sh switches the channel on. # OPENROUTER_API_KEY is gateway-only: init.sh writes no reference to it into # openclaw.json (a hand-added ${OPENROUTER_API_KEY} reference would need it here too). environment: OPENCLAW_GATEWAY_PASSWORD: "${OPENCLAW_GATEWAY_PASSWORD}" OPENCLAW_HOSTNAME: "${OPENCLAW_HOSTNAME:-openclaw.localhost}" OPENCLAW_USERNAME: "${OPENCLAW_USERNAME}" TELEGRAM_BOT_TOKEN: "${OPENCLAW_TELEGRAM_BOT_TOKEN:-}" # Marks this container as attached to openclaw-proxy only; init.sh refuses # to trust its own subnet without it (a stale pre-1.14.1 container). OPENCLAW_INIT_PROXY_NETWORK: "true" # Only the proxy network: init.sh reads that network's subnet from its own # interface and makes it the gateway's only trusted proxy range. networks: - openclaw-proxy volumes: - ./openclaw/init.sh:/openclaw-init.sh:ro - ./openclaw/ssh:/ssh-src:ro - openclaw_data:/home/node/.openclaw - openclaw_ssh:/home/node/.ssh openclaw: build: *openclaw-build image: localai-openclaw container_name: openclaw profiles: ["openclaw"] restart: unless-stopped logging: *default-logging init: true command: ["node", "dist/index.js", "gateway", "--bind", "lan", "--port", "18789"] environment: <<: *proxy-env OPENCLAW_GATEWAY_PASSWORD: "${OPENCLAW_GATEWAY_PASSWORD}" OPENCLAW_HOSTNAME: "${OPENCLAW_HOSTNAME:-openclaw.localhost}" OPENCLAW_USERNAME: "${OPENCLAW_USERNAME}" OPENROUTER_API_KEY: "${OPENCLAW_OPENROUTER_API_KEY:-}" TELEGRAM_BOT_TOKEN: "${OPENCLAW_TELEGRAM_BOT_TOKEN:-}" TZ: "${GENERIC_TIMEZONE:-UTC}" # Caddy reaches the gateway as openclaw-gateway, an alias that exists only on # openclaw-proxy, so its requests always come from the trusted subnet. networks: default: openclaw-proxy: aliases: - openclaw-gateway # GID of /var/run/docker.sock, written by 05_configure_services.sh group_add: - "${OPENCLAW_DOCKER_GID}" extra_hosts: - "host.docker.internal:host-gateway" volumes: - openclaw_data:/home/node/.openclaw - openclaw_ssh:/home/node/.ssh:ro - /var/run/docker.sock:/var/run/docker.sock healthcheck: test: ["CMD-SHELL", "http_proxy= https_proxy= HTTP_PROXY= HTTPS_PROXY= node dist/docker-healthcheck.js || exit 1"] interval: 30s timeout: 10s retries: 5 start_period: 60s depends_on: openclaw-init: condition: service_completed_successfully n8n-import: <<: *service-n8n container_name: n8n-import profiles: ["n8n"] environment: <<: *service-n8n-env RUN_N8N_IMPORT: "${RUN_N8N_IMPORT:-false}" entrypoint: /bin/sh command: /scripts/import_workflows.sh volumes: - ./n8n/backup:/backup - ./scripts/import_workflows.sh:/scripts/import_workflows.sh:ro depends_on: postgres: condition: service_healthy n8n: <<: *service-n8n container_name: n8n profiles: ["n8n"] restart: unless-stopped volumes: - n8n_storage:/home/node/.n8n - ./n8n/backup:/backup - ./shared:/data/shared healthcheck: test: ["CMD-SHELL", "http_proxy= https_proxy= HTTP_PROXY= HTTPS_PROXY= wget -qO- http://localhost:5678/healthz || exit 1"] interval: 30s timeout: 10s retries: 5 start_period: 30s depends_on: n8n-import: condition: service_completed_successfully # Template services for worker-runner pairs (used by docker-compose.n8n-workers.yml via extends) # These templates use profile "n8n-template" which is never activated directly n8n-worker-template: <<: *service-n8n profiles: ["n8n-template"] command: worker volumes: - n8n_storage:/home/node/.n8n - ./shared:/data/shared healthcheck: test: ["CMD-SHELL", "http_proxy= https_proxy= HTTP_PROXY= HTTPS_PROXY= wget -qO- http://localhost:5678/healthz || exit 1"] interval: 30s timeout: 10s retries: 5 start_period: 30s n8n-runner-template: <<: *service-n8n-worker-runner profiles: ["n8n-template"] entrypoint: ["/bin/sh", "-c", "/usr/local/bin/task-runner-launcher javascript python"] healthcheck: test: ["CMD-SHELL", "http_proxy= https_proxy= HTTP_PROXY= HTTPS_PROXY= wget -qO- http://localhost:5680/healthz || exit 1"] interval: 30s timeout: 10s retries: 5 start_period: 30s n8n-mcp: image: "${N8N_MCP_IMAGE:-ghcr.io/czlonkowski/n8n-mcp:latest}" container_name: n8n-mcp profiles: ["n8n-mcp"] restart: unless-stopped logging: *default-logging environment: <<: *proxy-env # The image defaults MCP_MODE to "stdio"; "http" is required to serve HTTP. # Do NOT set USE_FIXED_HTTP - upstream deprecated it and it breaks streaming. MCP_MODE: http NODE_ENV: production PORT: "3000" LOG_LEVEL: "${N8N_MCP_LOG_LEVEL:-info}" # Mandatory: the entrypoint exits 1 in http mode without a token. # Caddy gates on this same token, so one Bearer header satisfies both. AUTH_TOKEN: "${N8N_MCP_AUTH_TOKEN}" # Real client IPs behind Caddy. Without it the failed-auth rate limit is # shared by every client via Caddy's single container IP. TRUST_PROXY: "1" # What the server advertises as its own address. ':+' prepends the scheme # only when a hostname is set, so an unset N8N_MCP_HOSTNAME falls back to # the whole localhost URL rather than to a bare "https://". BASE_URL: "${N8N_MCP_HOSTNAME:+https://}${N8N_MCP_HOSTNAME:-http://localhost:3000}" # Anonymous telemetry is ENABLED by default upstream. N8N_MCP_TELEMETRY_DISABLED: "${N8N_MCP_TELEMETRY_DISABLED:-true}" # Blank N8N_API_KEY = documentation-only mode (node docs, schemas, # templates). Paste an n8n API key to unlock the workflow-management # tools. An empty value fails upstream's validation and is treated as # unconfigured, so the service still starts cleanly. N8N_API_URL: "${N8N_API_URL:-http://n8n:5678}" N8N_API_KEY: "${N8N_API_KEY:-}" # Optional. The access token of n8n's own MCP server (Settings -> # Instance-level MCP, n8n >= 2.34) - a separate secret from the Public # API key. It unlocks tools the Public API cannot serve (agents, dynamic # node resources, workflow version history, datatable columns). The # endpoint is derived from N8N_API_URL. Blank = Public API only, as before. N8N_MCP_ACCESS_TOKEN: "${N8N_MCP_ACCESS_TOKEN:-}" # n8n resolves to a private Docker IP, and n8n-MCP's own SSRF guard # rejects private ranges in both "strict" (upstream default) and # "moderate", which would fail every n8n API call. "permissive" still # blocks cloud-metadata endpoints. WEBHOOK_SECURITY_MODE: "${N8N_MCP_WEBHOOK_SECURITY_MODE:-permissive}" # Deliberately no volume: the node database is baked into the image and # re-seeded only when absent, so a named volume would mask the image copy # and freeze the node catalogue after every image update. The only state # here is a regenerable, read-mostly SQLite index. healthcheck: test: [ "CMD-SHELL", "http_proxy= https_proxy= HTTP_PROXY= HTTPS_PROXY= curl -fsS http://127.0.0.1:3000/health > /dev/null || exit 1", ] interval: 30s timeout: 10s retries: 5 start_period: 30s # --------------------------------------------------------------------------- # n8n Assistant sandbox (profile "n8n-sandbox") - upstream n8n-sandbox-service. # Runs the code the AI Assistant / Agents generate. Three services, no ports, # no Caddy block: n8n reaches sandbox-api by service name. The mTLS certs are # issued for the names "sandbox-api" and "sandbox-runner-1" - keep them. # The three images are released together; N8N_SANDBOX_VERSION selects one tag for all. # --------------------------------------------------------------------------- # One-shot: generates the CA and leaf certs into the shared volume, then exits. # Skips generation when the certs already exist, so it is safe on every start # (set SANDBOX_TLS_REGEN=1 on this service to force new certs). Keys stay 0600: # /tls/api is chowned to the api image's user, /tls/runner stays root's and the # runner runs as root (privileged, or ID-mapped root under sysbox-runc). sandbox-certs: image: "ghcr.io/n8n-io/n8n-sandbox-service-api:${N8N_SANDBOX_VERSION:-latest}" container_name: sandbox-certs profiles: ["n8n-sandbox"] restart: "no" logging: *default-logging user: "0:0" entrypoint: ["sh", "-c"] command: - > bootstrap-mtls.sh --out-dir /tls --api-san sandbox-api --control-san-prefix sandbox-runner && chown -R sandbox-api:sandbox-api /tls/api environment: NUM_RUNNERS: "1" volumes: - n8n_sandbox_tls:/tls sandbox-api: image: "ghcr.io/n8n-io/n8n-sandbox-service-api:${N8N_SANDBOX_VERSION:-latest}" container_name: sandbox-api profiles: ["n8n-sandbox"] restart: unless-stopped logging: *default-logging environment: # n8n authenticates with N8N_SANDBOX_SERVICE_API_KEY, which must be one of these. SANDBOX_API_KEYS: "${N8N_SANDBOX_API_KEY}" SANDBOX_API_RUNNER_REGISTRATION_TOKEN: "${N8N_SANDBOX_RUNNER_REGISTRATION_TOKEN}" SANDBOX_API_RUNNER_API_KEY: "${N8N_SANDBOX_RUNNER_API_KEY}" SANDBOX_API_GRPC_TLS_CERT_FILE: /tls/api/grpc-server.crt SANDBOX_API_GRPC_TLS_KEY_FILE: /tls/api/grpc-server.key SANDBOX_API_GRPC_TLS_CLIENT_CA_FILE: /tls/api/ca.crt SANDBOX_API_RUNNER_CONTROL_GRPC_TLS_CA_FILE: /tls/api/ca.crt SANDBOX_API_RUNNER_CONTROL_GRPC_TLS_CERT_FILE: /tls/api/control-grpc-api-client.crt SANDBOX_API_RUNNER_CONTROL_GRPC_TLS_KEY_FILE: /tls/api/control-grpc-api-client.key SANDBOX_API_RUNNER_CONTROL_GRPC_TLS_SERVER_NAME: sandbox-runner-1 volumes: - n8n_sandbox_tls:/tls:ro - n8n_sandbox_api_data:/var/lib/n8n-sandbox-api healthcheck: test: ["CMD-SHELL", "http_proxy= https_proxy= HTTP_PROXY= HTTPS_PROXY= wget -qO- http://localhost:8080/healthz || exit 1"] interval: 5s timeout: 3s retries: 5 start_period: 10s depends_on: sandbox-certs: condition: service_completed_successfully # Docker-in-Docker host for the sandboxes. Isolated with sysbox-runc when # 05_configure_services.sh could install it (N8N_SANDBOX_RUNNER_RUNTIME); with # the privileged fallback accepted there it runs privileged, which is # root-equivalent on the host. The shipped runc + false pair cannot run # Docker-in-Docker at all ('make doctor' flags it). Never publish its ports. # Deliberately no volume for the inner Docker: the sandbox image is # pulled on first use after each recreate, so it always matches the runner's tag # instead of a cached copy going stale behind it. sandbox-runner-1: image: "ghcr.io/n8n-io/n8n-sandbox-service-runner-dind:${N8N_SANDBOX_VERSION:-latest}" container_name: sandbox-runner-1 profiles: ["n8n-sandbox"] restart: unless-stopped logging: *default-logging runtime: "${N8N_SANDBOX_RUNNER_RUNTIME:-runc}" privileged: "${N8N_SANDBOX_RUNNER_PRIVILEGED:-false}" environment: SANDBOX_RUNNER_REGISTRATION_TOKEN: "${N8N_SANDBOX_RUNNER_REGISTRATION_TOKEN}" SANDBOX_RUNNER_API_KEYS: "${N8N_SANDBOX_RUNNER_API_KEY}" SANDBOX_RUNNER_API_GRPC_ADDR: sandbox-api:9090 SANDBOX_RUNNER_HTTP_BASE_URL: https://sandbox-runner-1:8080 SANDBOX_RUNNER_CONTROL_GRPC_LISTEN_ADDR: ":9091" SANDBOX_RUNNER_CONTROL_GRPC_ADVERTISE_ADDR: sandbox-runner-1:9091 SANDBOX_RUNNER_ID: runner-1 SANDBOX_RUNNER_DOCKER_SANDBOX_IMAGE: "ghcr.io/n8n-io/n8n-sandbox-service-sandbox:${N8N_SANDBOX_VERSION:-latest}" SANDBOX_RUNNER_REGISTRATION_GRPC_CA_FILE: /tls/runner/ca.crt SANDBOX_RUNNER_REGISTRATION_GRPC_CERT_FILE: /tls/runner/grpc-client.crt SANDBOX_RUNNER_REGISTRATION_GRPC_KEY_FILE: /tls/runner/grpc-client.key SANDBOX_RUNNER_REGISTRATION_GRPC_SERVER_NAME: sandbox-api SANDBOX_RUNNER_CONTROL_GRPC_TLS_CERT_FILE: /tls/runner/control-grpc-server.crt SANDBOX_RUNNER_CONTROL_GRPC_TLS_KEY_FILE: /tls/runner/control-grpc-server.key SANDBOX_RUNNER_CONTROL_GRPC_TLS_CLIENT_CA_FILE: /tls/runner/ca.crt volumes: - n8n_sandbox_tls:/tls:ro # The runner listener is TLS-only, hence https and --no-check-certificate. healthcheck: test: ["CMD-SHELL", "http_proxy= https_proxy= HTTP_PROXY= HTTPS_PROXY= wget -q -O /dev/null --no-check-certificate https://localhost:8080/readyz || exit 1"] interval: 5s timeout: 5s retries: 24 start_period: 20s depends_on: sandbox-api: condition: service_healthy qdrant: image: qdrant/qdrant container_name: qdrant profiles: ["qdrant"] restart: unless-stopped volumes: - qdrant_storage:/qdrant/storage environment: QDRANT__SERVICE__API_KEY: "${QDRANT_API_KEY}" expose: - "6333" neo4j: image: neo4j:latest container_name: neo4j profiles: ["neo4j"] restart: unless-stopped volumes: - ./neo4j/logs:/logs - ./neo4j/config:/config - ./neo4j/data:/data - ./neo4j/plugins:/plugins environment: - "NEO4J_AUTH=${NEO4J_AUTH_USERNAME}/${NEO4J_AUTH_PASSWORD}" healthcheck: test: [ "CMD-SHELL", "wget --no-verbose --tries=1 --spider http://localhost:7474 || exit 1", ] interval: 5s timeout: 3s retries: 5 ulimits: nofile: soft: 40000 hard: 40000 nocodb: image: nocodb/nocodb:latest container_name: nocodb profiles: ["nocodb"] restart: unless-stopped logging: *default-logging environment: # NocoDB rejects external data sources whose host resolves to a private # range (SSRF protection, GHSA-w43h-r5m5-p832), which covers every # hostname on this Docker network - including "postgres". The check first # appears in helpers/validateDbConnectionHost.ts at tag 2026.05.2; the # advisory metadata says 2026.05.1, and the two genuinely disagree. # This opt-in is deliberately the narrow one: it re-allows internal # databases only. Webhooks stay gated by NC_ALLOW_LOCAL_HOOKS, and on # 2026.07.0+ data import by NC_ALLOW_LOCAL_DATA_IMPORT (before that # release, import was gated by NC_ALLOW_LOCAL_HOOKS too). Never swap this # for NC_DISABLE_SSRF_PROTECTION, which turns all of them off. # Compared with === 'true', so it must be the lowercase string. NC_ALLOW_LOCAL_EXTERNAL_DBS: "true" NC_AUTH_JWT_SECRET: "${NOCODB_JWT_SECRET}" NC_DB: "pg://postgres:5432?u=postgres&p=${POSTGRES_PASSWORD}&d=nocodb" NC_PUBLIC_URL: "https://${NOCODB_HOSTNAME}" NC_REDIS_URL: redis://redis:6379/1 volumes: - nocodb_data:/usr/app/data healthcheck: test: ["CMD-SHELL", "wget -qO- http://localhost:8080/api/v1/health || exit 1"] interval: 30s timeout: 10s retries: 5 start_period: 30s depends_on: postgres: condition: service_healthy redis: condition: service_healthy caddy: container_name: caddy image: docker.io/library/caddy:2-alpine networks: - default - openclaw-proxy ports: - "80:80" - "443:443" # Bolt (7687) is published by docker-compose.neo4j.yml, only while the # neo4j profile is active (issue #134) - do not add it back here. restart: unless-stopped extra_hosts: - "host.docker.internal:host-gateway" volumes: - ./Caddyfile:/etc/caddy/Caddyfile:ro - ./caddy-addon:/etc/caddy/addons:ro - ./certs:/etc/caddy/certs:ro - ./welcome:/srv/welcome:ro - caddy-data:/data:rw - caddy-config:/config:rw environment: APPSMITH_HOSTNAME: "${APPSMITH_HOSTNAME}" COMFYUI_HOSTNAME: "${COMFYUI_HOSTNAME}" COMFYUI_PASSWORD_HASH: "${COMFYUI_PASSWORD_HASH}" COMFYUI_USERNAME: "${COMFYUI_USERNAME}" DIFY_HOSTNAME: "${DIFY_HOSTNAME}" DOCLING_HOSTNAME: "${DOCLING_HOSTNAME}" DOCLING_PASSWORD_HASH: "${DOCLING_PASSWORD_HASH}" DOCLING_USERNAME: "${DOCLING_USERNAME}" FLOWISE_HOSTNAME: "${FLOWISE_HOSTNAME}" GRAFANA_HOSTNAME: "${GRAFANA_HOSTNAME}" INVOKEAI_HOSTNAME: "${INVOKEAI_HOSTNAME}" INVOKEAI_PASSWORD_HASH: "${INVOKEAI_PASSWORD_HASH}" INVOKEAI_USERNAME: "${INVOKEAI_USERNAME}" KEELFLOW_HOSTNAME: "${KEELFLOW_HOSTNAME}" LANGFUSE_HOSTNAME: "${LANGFUSE_HOSTNAME}" LETSENCRYPT_EMAIL: "${LETSENCRYPT_EMAIL:-internal}" LETTA_HOSTNAME: "${LETTA_HOSTNAME}" LIGHTRAG_HOSTNAME: "${LIGHTRAG_HOSTNAME}" LT_HOSTNAME: "${LT_HOSTNAME}" LT_PASSWORD_HASH: "${LT_PASSWORD_HASH}" LT_USERNAME: "${LT_USERNAME}" N8N_HOSTNAME: "${N8N_HOSTNAME}" N8N_MCP_AUTH_TOKEN: "${N8N_MCP_AUTH_TOKEN}" # Defaulted, unlike the other hostnames: 'git pull' + 'make restart' does # not add new .env variables, so an .env from before this service existed # would expand to an empty Caddy site address, which is a Caddyfile PARSE # error - Caddy would crash-loop and take HTTPS down for every service, # not just this one. '.localhost' gets an internal cert, so no ACME noise. N8N_MCP_HOSTNAME: "${N8N_MCP_HOSTNAME:-n8n-mcp.localhost}" NEO4J_HOSTNAME: "${NEO4J_HOSTNAME}" NOCODB_HOSTNAME: "${NOCODB_HOSTNAME}" OLLAMA_HOSTNAME: "${OLLAMA_HOSTNAME}" OLLAMA_CADDY_API_TOKEN: "${OLLAMA_CADDY_API_TOKEN}" # Defaulted for the same reason as N8N_MCP_HOSTNAME above. OPENCLAW_HOSTNAME: "${OPENCLAW_HOSTNAME:-openclaw.localhost}" OPENCLAW_PASSWORD_HASH: "${OPENCLAW_PASSWORD_HASH}" OPENCLAW_USERNAME: "${OPENCLAW_USERNAME}" PADDLEOCR_HOSTNAME: "${PADDLEOCR_HOSTNAME}" PADDLEOCR_PASSWORD_HASH: "${PADDLEOCR_PASSWORD_HASH}" PADDLEOCR_USERNAME: "${PADDLEOCR_USERNAME}" PORTAINER_HOSTNAME: "${PORTAINER_HOSTNAME}" DATABASUS_HOSTNAME: "${DATABASUS_HOSTNAME}" POSTIZ_HOSTNAME: "${POSTIZ_HOSTNAME}" TEMPORAL_UI_HOSTNAME: "${TEMPORAL_UI_HOSTNAME}" TEMPORAL_UI_USERNAME: "${TEMPORAL_UI_USERNAME}" TEMPORAL_UI_PASSWORD_HASH: "${TEMPORAL_UI_PASSWORD_HASH}" PROMETHEUS_HOSTNAME: "${PROMETHEUS_HOSTNAME}" PROMETHEUS_PASSWORD_HASH: "${PROMETHEUS_PASSWORD_HASH}" PROMETHEUS_USERNAME: "${PROMETHEUS_USERNAME}" QDRANT_HOSTNAME: "${QDRANT_HOSTNAME}" RAGAPP_HOSTNAME: "${RAGAPP_HOSTNAME}" RAGAPP_PASSWORD_HASH: "${RAGAPP_PASSWORD_HASH}" RAGAPP_USERNAME: "${RAGAPP_USERNAME}" RAGFLOW_HOSTNAME: "${RAGFLOW_HOSTNAME}" SEARXNG_HOSTNAME: "${SEARXNG_HOSTNAME}" SEARXNG_PASSWORD_HASH: "${SEARXNG_PASSWORD_HASH}" SEARXNG_USERNAME: "${SEARXNG_USERNAME}" SUPABASE_HOSTNAME: "${SUPABASE_HOSTNAME}" UPTIME_KUMA_HOSTNAME: "${UPTIME_KUMA_HOSTNAME}" WAHA_HOSTNAME: "${WAHA_HOSTNAME}" WEAVIATE_HOSTNAME: "${WEAVIATE_HOSTNAME}" WEBUI_HOSTNAME: "${WEBUI_HOSTNAME}" WELCOME_HOSTNAME: "${WELCOME_HOSTNAME}" WELCOME_PASSWORD_HASH: "${WELCOME_PASSWORD_HASH}" WELCOME_USERNAME: "${WELCOME_USERNAME}" cap_drop: - ALL cap_add: - NET_BIND_SERVICE logging: *default-logging cloudflared: image: cloudflare/cloudflared:latest container_name: cloudflared profiles: ["cloudflare-tunnel"] restart: unless-stopped command: "tunnel --no-autoupdate run --protocol ${CLOUDFLARE_TUNNEL_PROTOCOL:-auto}" environment: TUNNEL_TOKEN: "${CLOUDFLARE_TUNNEL_TOKEN}" logging: *default-logging gost: image: gogost/gost:latest container_name: gost profiles: ["gost"] restart: unless-stopped command: - "-L" - "http://${GOST_USERNAME}:${GOST_PASSWORD}@:8080" - "-F" - "${GOST_UPSTREAM_PROXY}" healthcheck: test: ["CMD-SHELL", "nc -z localhost 8080 || exit 1"] interval: 60s timeout: 10s retries: 3 start_period: 10s logging: *default-logging langfuse-worker: image: langfuse/langfuse-worker:3 container_name: langfuse-worker restart: always profiles: ["langfuse"] depends_on: &langfuse-depends-on postgres: condition: service_healthy minio: condition: service_healthy redis: condition: service_healthy clickhouse: condition: service_healthy environment: &langfuse-worker-env <<: *proxy-env DATABASE_URL: "postgresql://postgres:${POSTGRES_PASSWORD}@postgres:5432/langfuse" SALT: "${LANGFUSE_SALT}" ENCRYPTION_KEY: "${ENCRYPTION_KEY}" TELEMETRY_ENABLED: "${TELEMETRY_ENABLED:-true}" LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES: "${LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES:-true}" CLICKHOUSE_MIGRATION_URL: "${CLICKHOUSE_MIGRATION_URL:-clickhouse://clickhouse:9000}" CLICKHOUSE_URL: "${CLICKHOUSE_URL:-http://clickhouse:8123}" CLICKHOUSE_USER: "${CLICKHOUSE_USER:-clickhouse}" CLICKHOUSE_PASSWORD: "${CLICKHOUSE_PASSWORD}" CLICKHOUSE_CLUSTER_ENABLED: "${CLICKHOUSE_CLUSTER_ENABLED:-false}" LANGFUSE_S3_EVENT_UPLOAD_BUCKET: "${LANGFUSE_S3_EVENT_UPLOAD_BUCKET:-langfuse}" LANGFUSE_S3_EVENT_UPLOAD_REGION: "${LANGFUSE_S3_EVENT_UPLOAD_REGION:-auto}" LANGFUSE_S3_EVENT_UPLOAD_ACCESS_KEY_ID: "${LANGFUSE_S3_EVENT_UPLOAD_ACCESS_KEY_ID:-minio}" LANGFUSE_S3_EVENT_UPLOAD_SECRET_ACCESS_KEY: "${MINIO_ROOT_PASSWORD}" LANGFUSE_S3_EVENT_UPLOAD_ENDPOINT: "${LANGFUSE_S3_EVENT_UPLOAD_ENDPOINT:-http://minio:9000}" LANGFUSE_S3_EVENT_UPLOAD_FORCE_PATH_STYLE: "${LANGFUSE_S3_EVENT_UPLOAD_FORCE_PATH_STYLE:-true}" LANGFUSE_S3_EVENT_UPLOAD_PREFIX: "${LANGFUSE_S3_EVENT_UPLOAD_PREFIX:-events/}" LANGFUSE_S3_MEDIA_UPLOAD_BUCKET: "${LANGFUSE_S3_MEDIA_UPLOAD_BUCKET:-langfuse}" LANGFUSE_S3_MEDIA_UPLOAD_REGION: "${LANGFUSE_S3_MEDIA_UPLOAD_REGION:-auto}" LANGFUSE_S3_MEDIA_UPLOAD_ACCESS_KEY_ID: "${LANGFUSE_S3_MEDIA_UPLOAD_ACCESS_KEY_ID:-minio}" LANGFUSE_S3_MEDIA_UPLOAD_SECRET_ACCESS_KEY: "${MINIO_ROOT_PASSWORD}" LANGFUSE_S3_MEDIA_UPLOAD_ENDPOINT: "${LANGFUSE_S3_MEDIA_UPLOAD_ENDPOINT:-http://localhost:9090}" LANGFUSE_S3_MEDIA_UPLOAD_FORCE_PATH_STYLE: "${LANGFUSE_S3_MEDIA_UPLOAD_FORCE_PATH_STYLE:-true}" LANGFUSE_S3_MEDIA_UPLOAD_PREFIX: "${LANGFUSE_S3_MEDIA_UPLOAD_PREFIX:-media/}" LANGFUSE_S3_BATCH_EXPORT_ENABLED: "${LANGFUSE_S3_BATCH_EXPORT_ENABLED:-false}" LANGFUSE_S3_BATCH_EXPORT_BUCKET: "${LANGFUSE_S3_BATCH_EXPORT_BUCKET:-langfuse}" LANGFUSE_S3_BATCH_EXPORT_PREFIX: "${LANGFUSE_S3_BATCH_EXPORT_PREFIX:-exports/}" LANGFUSE_S3_BATCH_EXPORT_REGION: "${LANGFUSE_S3_BATCH_EXPORT_REGION:-auto}" LANGFUSE_S3_BATCH_EXPORT_ENDPOINT: "${LANGFUSE_S3_BATCH_EXPORT_ENDPOINT:-http://minio:9000}" LANGFUSE_S3_BATCH_EXPORT_EXTERNAL_ENDPOINT: "${LANGFUSE_S3_BATCH_EXPORT_EXTERNAL_ENDPOINT:-http://localhost:9090}" LANGFUSE_S3_BATCH_EXPORT_ACCESS_KEY_ID: "${LANGFUSE_S3_BATCH_EXPORT_ACCESS_KEY_ID:-minio}" LANGFUSE_S3_BATCH_EXPORT_SECRET_ACCESS_KEY: "${MINIO_ROOT_PASSWORD}" LANGFUSE_S3_BATCH_EXPORT_FORCE_PATH_STYLE: "${LANGFUSE_S3_BATCH_EXPORT_FORCE_PATH_STYLE:-true}" LANGFUSE_INGESTION_QUEUE_DELAY_MS: "${LANGFUSE_INGESTION_QUEUE_DELAY_MS:-}" LANGFUSE_INGESTION_CLICKHOUSE_WRITE_INTERVAL_MS: "${LANGFUSE_INGESTION_CLICKHOUSE_WRITE_INTERVAL_MS:-}" REDIS_HOST: "${REDIS_HOST:-redis}" REDIS_PORT: "${REDIS_PORT:-6379}" REDIS_AUTH: "${REDIS_AUTH:-}" REDIS_TLS_ENABLED: "${REDIS_TLS_ENABLED:-false}" REDIS_TLS_CA: "${REDIS_TLS_CA:-/certs/ca.crt}" REDIS_TLS_CERT: "${REDIS_TLS_CERT:-/certs/redis.crt}" REDIS_TLS_KEY: "${REDIS_TLS_KEY:-/certs/redis.key}" langfuse-web: image: langfuse/langfuse:3 container_name: langfuse-web restart: always profiles: ["langfuse"] depends_on: *langfuse-depends-on environment: <<: *langfuse-worker-env NEXTAUTH_URL: "https://${LANGFUSE_HOSTNAME}" NEXTAUTH_SECRET: "${NEXTAUTH_SECRET}" LANGFUSE_INIT_ORG_ID: "${LANGFUSE_INIT_ORG_ID:-organization_id}" LANGFUSE_INIT_ORG_NAME: "${LANGFUSE_INIT_ORG_NAME:-Organization}" LANGFUSE_INIT_PROJECT_ID: "${LANGFUSE_INIT_PROJECT_ID:-project_id}" LANGFUSE_INIT_PROJECT_NAME: "${LANGFUSE_INIT_PROJECT_NAME:-Project}" LANGFUSE_INIT_PROJECT_PUBLIC_KEY: "${LANGFUSE_INIT_PROJECT_PUBLIC_KEY:-}" LANGFUSE_INIT_PROJECT_SECRET_KEY: "${LANGFUSE_INIT_PROJECT_SECRET_KEY:-}" LANGFUSE_INIT_USER_EMAIL: "${LANGFUSE_INIT_USER_EMAIL:-}" LANGFUSE_INIT_USER_NAME: "${LANGFUSE_INIT_USER_NAME:-}" LANGFUSE_INIT_USER_PASSWORD: "${LANGFUSE_INIT_USER_PASSWORD:-}" AUTH_DISABLE_SIGNUP: "${AUTH_DISABLE_SIGNUP:-true}" clickhouse: image: clickhouse/clickhouse-server container_name: clickhouse restart: always profiles: ["langfuse"] user: "101:101" environment: CLICKHOUSE_DB: default CLICKHOUSE_USER: clickhouse CLICKHOUSE_PASSWORD: "${CLICKHOUSE_PASSWORD}" volumes: - langfuse_clickhouse_data:/var/lib/clickhouse - langfuse_clickhouse_logs:/var/log/clickhouse-server healthcheck: test: wget --no-verbose --tries=1 --spider http://localhost:8123/ping || exit 1 interval: 5s timeout: 5s retries: 10 start_period: 1s minio: # Silo: maintained MinIO fork with the same data format and MINIO_* vars # (quay.io/minio/minio rejects anonymous pulls). The server binary is # 'silo', with no 'minio' alias. Not the 'distroless' tag: it has no sh/curl. image: docker.io/pgsty/silo:latest container_name: minio restart: always profiles: ["langfuse"] entrypoint: sh # create the 'langfuse' bucket before starting the service command: -c 'mkdir -p /data/langfuse && silo server --address ":9000" --console-address ":9001" /data' environment: MINIO_ROOT_USER: minio MINIO_ROOT_PASSWORD: "${MINIO_ROOT_PASSWORD}" volumes: - langfuse_minio_data:/data healthcheck: test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/cluster"] interval: 1s timeout: 5s retries: 5 start_period: 1s postgres: container_name: postgres image: "pgvector/pgvector:pg${POSTGRES_VERSION:-17}" restart: unless-stopped healthcheck: test: ["CMD-SHELL", "pg_isready -U postgres"] interval: 3s timeout: 3s retries: 10 environment: POSTGRES_USER: postgres POSTGRES_PASSWORD: "${POSTGRES_PASSWORD}" POSTGRES_DB: postgres volumes: - postgres_data:/var/lib/postgresql/data redis: container_name: redis image: docker.io/valkey/valkey:8-alpine command: valkey-server --save 30 1 --loglevel warning restart: unless-stopped stop_grace_period: 30s volumes: - valkey-data:/data cap_drop: - ALL cap_add: - SETGID - SETUID - DAC_OVERRIDE logging: *default-logging healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 3s timeout: 10s retries: 10 searxng: container_name: searxng image: docker.io/searxng/searxng:latest profiles: ["searxng"] restart: unless-stopped volumes: - ./searxng:/etc/searxng:rw environment: SEARXNG_BASE_URL: "https://${SEARXNG_HOSTNAME:-localhost}/" UWSGI_WORKERS: "${SEARXNG_UWSGI_WORKERS:-4}" UWSGI_THREADS: "${SEARXNG_UWSGI_THREADS:-4}" # cap_drop: - ALL # Temporarily commented out for first run cap_add: - CHOWN - SETGID - SETUID logging: *default-logging ollama-cpu: profiles: ["cpu"] <<: *service-ollama ollama-gpu: profiles: ["gpu-nvidia"] <<: *service-ollama deploy: resources: reservations: devices: - driver: nvidia count: "${OLLAMA_GPU_COUNT:-1}" capabilities: [gpu] ollama-gpu-amd: profiles: ["gpu-amd"] <<: *service-ollama image: ollama/ollama:rocm devices: - "/dev/kfd" - "/dev/dri" # Templates for extra Ollama instances on multi-GPU hosts, used by # docker-compose.ollama-instances.yml via 'extends'. The "ollama-template" # profile is never activated directly. # # These deliberately carry NO 'deploy:' block. Compose APPENDS # deploy.resources.reservations.devices across 'extends', so a count-based # reservation here would leak an extra, arbitrary GPU into every instance # that pins specific device IDs. Do not add one. ollama-instance-template: <<: *service-ollama container_name: ollama-instance-template profiles: ["ollama-template"] ollama-instance-template-amd: <<: *service-ollama container_name: ollama-instance-template-amd profiles: ["ollama-template"] image: ollama/ollama:rocm devices: - "/dev/kfd" - "/dev/dri" ollama-pull-llama-cpu: profiles: ["cpu"] <<: *init-ollama depends_on: - ollama-cpu ollama-pull-llama-gpu: profiles: ["gpu-nvidia"] <<: *init-ollama depends_on: - ollama-gpu ollama-pull-llama-gpu-amd: profiles: [gpu-amd] <<: *init-ollama image: ollama/ollama:rocm depends_on: - ollama-gpu-amd invokeai-nvidia: <<: *service-invokeai image: ghcr.io/invoke-ai/invokeai:latest profiles: ["invokeai-nvidia"] deploy: resources: reservations: devices: - driver: nvidia count: "${INVOKEAI_GPU_COUNT:-1}" capabilities: [gpu] invokeai-amd: <<: *service-invokeai image: ghcr.io/invoke-ai/invokeai:main-rocm profiles: ["invokeai-amd"] devices: - "/dev/kfd" - "/dev/dri" environment: <<: *invokeai-env AMD_VISIBLE_DEVICES: all RENDER_GROUP_ID: "${RENDER_GROUP_ID:-}" invokeai-cpu: <<: *service-invokeai image: ghcr.io/invoke-ai/invokeai:main-cpu profiles: ["invokeai-cpu"] prometheus: image: prom/prometheus:latest container_name: prometheus profiles: ["monitoring"] restart: unless-stopped volumes: - ./prometheus:/etc/prometheus:ro - prometheus_data:/prometheus extra_hosts: - "host.docker.internal:host-gateway" node-exporter: image: prom/node-exporter:latest container_name: node-exporter profiles: ["monitoring"] restart: unless-stopped volumes: - /proc:/host/proc:ro - /sys:/host/sys:ro - /:/rootfs:ro command: - "--path.procfs=/host/proc" - "--path.sysfs=/host/sys" - "--collector.filesystem.mount-points-exclude=^/(sys|proc|dev|host|etc)($$|/)" expose: - 9100 cadvisor: image: gcr.io/cadvisor/cadvisor:v0.55.1 container_name: cadvisor profiles: ["monitoring"] restart: unless-stopped mem_limit: 1g mem_reservation: 256m cpus: "1.0" command: - --housekeeping_interval=30s - --docker_only=true # disk (du over every container's layers) is costly with many images. The flag # replaces cAdvisor's default list, so it repeats the v0.55.1 one; re-sync on bumps. - --disable_metrics=advtcp,cpu_topology,cpuset,disk,diskIO,hugetlb,memory_numa,process,referenced_memory,resctrl,sched,tcp,udp volumes: - /:/rootfs:ro - /var/run:/var/run:rw - /sys:/sys:ro - /var/lib/docker/:/var/lib/docker:ro expose: - 8080 grafana: image: grafana/grafana:latest container_name: grafana profiles: ["monitoring"] restart: unless-stopped environment: GF_SECURITY_ADMIN_PASSWORD: "${GRAFANA_ADMIN_PASSWORD:-admin}" GF_PROVISIONING_PATH: /etc/grafana/provisioning volumes: - grafana:/var/lib/grafana - ./grafana/provisioning:/etc/grafana/provisioning - ./grafana/dashboards:/var/lib/grafana/dashboards # Standard path often used, let's use Grafana's managed dashboards dir extra_hosts: - "host.docker.internal:host-gateway" depends_on: - prometheus crawl4ai: image: unclecode/crawl4ai:latest # Use official image container_name: crawl4ai profiles: ["crawl4ai"] restart: unless-stopped shm_size: 1g # Recommended for browser operations # No 'env_file: .env' here: it would hand every secret of the stack to the # crawler. Pass the variables it needs explicitly. environment: <<: *proxy-env # Without a token Crawl4AI 0.9+ binds 127.0.0.1 only and is unreachable # from other containers; with it the API listens on the Docker network. CRAWL4AI_API_TOKEN: "${CRAWL4AI_API_TOKEN}" # Key for the default LLM provider (openai/gpt-4o-mini). OPENAI_API_KEY: "${OPENAI_API_KEY:-}" deploy: resources: limits: cpus: "1.0" memory: 4G # Increased based on documentation recommendation gotenberg: image: gotenberg/gotenberg:8 container_name: gotenberg profiles: ["gotenberg"] restart: unless-stopped environment: DISABLE_GOOGLE_CHROME: false healthcheck: test: ["CMD", "curl", "-fsS", "http://localhost:3000/health"] interval: 30s timeout: 10s retries: 3 letta: image: letta/letta:latest container_name: letta profiles: ["letta"] restart: unless-stopped volumes: - letta_data:/var/lib/postgresql/data environment: <<: *proxy-env OPENAI_API_KEY: "${OPENAI_API_KEY:-}" ANTHROPIC_API_KEY: "${ANTHROPIC_API_KEY:-}" OLLAMA_BASE_URL: "${OLLAMA_BASE_URL:-}" SECURE: "${LETTA_SECURE:-true}" LETTA_SERVER_PASSWORD: "${LETTA_SERVER_PASSWORD:-}" extra_hosts: - "host.docker.internal:host-gateway" weaviate: image: cr.weaviate.io/semitechnologies/weaviate:latest container_name: weaviate profiles: ["weaviate"] restart: unless-stopped volumes: - weaviate_data:/var/lib/weaviate environment: QUERY_DEFAULTS_LIMIT: 25 AUTHENTICATION_ANONYMOUS_ACCESS_ENABLED: "false" AUTHENTICATION_APIKEY_ENABLED: "true" AUTHENTICATION_APIKEY_ALLOWED_KEYS: "${WEAVIATE_API_KEY}" AUTHENTICATION_APIKEY_USERS: "${WEAVIATE_USERNAME}" AUTHORIZATION_ENABLE_RBAC: "true" AUTHORIZATION_RBAC_ROOT_USERS: "${WEAVIATE_USERNAME}" PERSISTENCE_DATA_PATH: "/var/lib/weaviate" ENABLE_API_BASED_MODULES: "true" CLUSTER_HOSTNAME: "node1" DEFAULT_VECTORIZER_MODULE: "none" healthcheck: test: [ "CMD-SHELL", "wget -q --spider http://localhost:8080/v1/.well-known/ready || exit 1", ] interval: 10s timeout: 5s retries: 5 start_period: 10s ragapp: image: ragapp/ragapp:latest container_name: ragapp profiles: ["ragapp"] restart: unless-stopped environment: <<: *proxy-env portainer: image: portainer/portainer-ce:latest container_name: portainer profiles: ["portainer"] restart: unless-stopped volumes: - portainer_data:/data - "${DOCKER_SOCKET_LOCATION:-/var/run/docker.sock}:/var/run/docker.sock" temporal-elasticsearch: image: elasticsearch:7.17.27 container_name: temporal-elasticsearch profiles: ["postiz"] restart: unless-stopped logging: *default-logging environment: cluster.routing.allocation.disk.threshold_enabled: "true" cluster.routing.allocation.disk.watermark.low: 512mb cluster.routing.allocation.disk.watermark.high: 256mb cluster.routing.allocation.disk.watermark.flood_stage: 128mb discovery.type: single-node ES_JAVA_OPTS: -Xms512m -Xmx512m xpack.security.enabled: "false" volumes: - temporal_elasticsearch_data:/usr/share/elasticsearch/data healthcheck: test: ["CMD-SHELL", "curl -s http://localhost:9200/_cluster/health | grep -qE '\"status\":\"(green|yellow)\"'"] interval: 30s timeout: 10s retries: 5 start_period: 60s temporal: image: temporalio/auto-setup:latest container_name: temporal profiles: ["postiz"] restart: unless-stopped logging: *default-logging environment: DB: postgres12 POSTGRES_USER: postgres POSTGRES_PWD: "${POSTGRES_PASSWORD}" POSTGRES_SEEDS: postgres DB_PORT: 5432 TEMPORAL_NAMESPACE: default ENABLE_ES: "true" ES_SEEDS: temporal-elasticsearch ES_VERSION: v7 depends_on: postgres: condition: service_healthy temporal-elasticsearch: condition: service_healthy healthcheck: test: ["CMD-SHELL", "temporal operator cluster health --address $(hostname -i):7233 | grep -q SERVING || exit 1"] interval: 30s timeout: 10s retries: 5 start_period: 60s temporal-ui: image: temporalio/ui:latest container_name: temporal-ui profiles: ["postiz"] restart: unless-stopped logging: *default-logging environment: TEMPORAL_ADDRESS: temporal:7233 TEMPORAL_CORS_ORIGINS: http://localhost:3000 depends_on: temporal: condition: service_healthy postiz: image: ghcr.io/gitroomhq/postiz-app:latest container_name: postiz profiles: ["postiz"] restart: always environment: <<: *proxy-env BACKEND_INTERNAL_URL: http://localhost:3000 DATABASE_URL: "postgresql://postgres:${POSTGRES_PASSWORD}@postgres:5432/${POSTIZ_DB_NAME:-postiz}?schema=postiz" DISABLE_REGISTRATION: "${POSTIZ_DISABLE_REGISTRATION}" FRONTEND_URL: "${POSTIZ_HOSTNAME:+https://}${POSTIZ_HOSTNAME}" IS_GENERAL: "true" # Required for self-hosting. JWT_SECRET: "${JWT_SECRET}" MAIN_URL: "${POSTIZ_HOSTNAME:+https://}${POSTIZ_HOSTNAME}" NEXT_PUBLIC_BACKEND_URL: "${POSTIZ_HOSTNAME:+https://}${POSTIZ_HOSTNAME}/api" NEXT_PUBLIC_UPLOAD_DIRECTORY: "/uploads" REDIS_URL: "redis://redis:6379" STORAGE_PROVIDER: "local" TEMPORAL_ADDRESS: temporal:7233 UPLOAD_DIRECTORY: "/uploads" # Social Media API Settings X_API_KEY: "${X_API_KEY}" X_API_SECRET: "${X_API_SECRET}" LINKEDIN_CLIENT_ID: "${LINKEDIN_CLIENT_ID}" LINKEDIN_CLIENT_SECRET: "${LINKEDIN_CLIENT_SECRET}" REDDIT_CLIENT_ID: "${REDDIT_CLIENT_ID}" REDDIT_CLIENT_SECRET: "${REDDIT_CLIENT_SECRET}" GITHUB_CLIENT_ID: "${GITHUB_CLIENT_ID}" GITHUB_CLIENT_SECRET: "${GITHUB_CLIENT_SECRET}" BEEHIIVE_API_KEY: "${BEEHIIVE_API_KEY}" BEEHIIVE_PUBLICATION_ID: "${BEEHIIVE_PUBLICATION_ID}" THREADS_APP_ID: "${THREADS_APP_ID}" THREADS_APP_SECRET: "${THREADS_APP_SECRET}" FACEBOOK_APP_ID: "${FACEBOOK_APP_ID}" FACEBOOK_APP_SECRET: "${FACEBOOK_APP_SECRET}" YOUTUBE_CLIENT_ID: "${YOUTUBE_CLIENT_ID}" YOUTUBE_CLIENT_SECRET: "${YOUTUBE_CLIENT_SECRET}" TIKTOK_CLIENT_ID: "${TIKTOK_CLIENT_ID}" TIKTOK_CLIENT_SECRET: "${TIKTOK_CLIENT_SECRET}" PINTEREST_CLIENT_ID: "${PINTEREST_CLIENT_ID}" PINTEREST_CLIENT_SECRET: "${PINTEREST_CLIENT_SECRET}" DRIBBBLE_CLIENT_ID: "${DRIBBBLE_CLIENT_ID}" DRIBBBLE_CLIENT_SECRET: "${DRIBBBLE_CLIENT_SECRET}" DISCORD_CLIENT_ID: "${DISCORD_CLIENT_ID}" DISCORD_CLIENT_SECRET: "${DISCORD_CLIENT_SECRET}" DISCORD_BOT_TOKEN_ID: "${DISCORD_BOT_TOKEN_ID}" SLACK_ID: "${SLACK_ID}" SLACK_SECRET: "${SLACK_SECRET}" SLACK_SIGNING_SECRET: "${SLACK_SIGNING_SECRET}" MASTODON_URL: "${MASTODON_URL}" MASTODON_CLIENT_ID: "${MASTODON_CLIENT_ID}" MASTODON_CLIENT_SECRET: "${MASTODON_CLIENT_SECRET}" volumes: - postiz-config:/config/ - postiz-uploads:/uploads/ - ./postiz.env:/app/.env:ro depends_on: postgres: condition: service_healthy redis: condition: service_healthy temporal: condition: service_healthy databasus: image: databasus/databasus:latest container_name: databasus profiles: ["databasus"] restart: unless-stopped logging: *default-logging volumes: - databasus_data:/databasus-data healthcheck: test: ["CMD", "databasus", "healthcheck"] interval: 30s timeout: 10s retries: 5 start_period: 60s # ComfyUI hardware variants (mutually exclusive profiles, like InvokeAI). # Deliberately NO CLI_ARGS here: each image ships the right default # ("--cpu" baked into the cpu image, empty on the GPU images) and the # entrypoint already adds --listen --port 8188. An explicit CLI_ARGS: "" # would strip --cpu from the CPU image. comfyui-nvidia: <<: *service-comfyui image: yanwk/comfyui-boot:cu126-slim profiles: ["comfyui-nvidia"] deploy: resources: reservations: devices: - driver: nvidia count: "${COMFYUI_GPU_COUNT:-1}" capabilities: [gpu] comfyui-amd: <<: *service-comfyui image: yanwk/comfyui-boot:rocm profiles: ["comfyui-amd"] devices: - "/dev/kfd" - "/dev/dri" comfyui-cpu: <<: *service-comfyui image: yanwk/comfyui-boot:cpu profiles: ["comfyui-cpu"] libretranslate: image: libretranslate/libretranslate:latest container_name: libretranslate profiles: ["libretranslate"] restart: unless-stopped environment: LT_API_KEYS: "${LT_API_KEYS:-false}" LT_BATCH_LIMIT: "${LT_BATCH_LIMIT:-}" LT_CHAR_LIMIT: "${LT_CHAR_LIMIT:-10000}" LT_DEBUG: "${LT_DEBUG:-false}" LT_FRONTEND_LANGUAGE_SOURCE: "${LT_FRONTEND_LANGUAGE_SOURCE:-auto}" LT_FRONTEND_LANGUAGE_TARGET: "${LT_FRONTEND_LANGUAGE_TARGET:-en}" LT_FRONTEND_TIMEOUT: "${LT_FRONTEND_TIMEOUT:-2000}" LT_HOST: "${LT_HOST:-0.0.0.0}" LT_LOAD_ONLY: "${LT_LOAD_ONLY:-}" LT_METRICS: "${LT_METRICS:-false}" LT_PORT: "${LT_PORT:-5000}" LT_REQ_LIMIT: "${LT_REQ_LIMIT:-}" LT_SSL: "${LT_SSL:-false}" LT_SUGGESTIONS: "${LT_SUGGESTIONS:-false}" LT_THREADS: "${LT_THREADS:-4}" LT_UPDATE_MODELS: "${LT_UPDATE_MODELS:-false}" volumes: - libretranslate_api_keys:/app/db - libretranslate_models:/home/libretranslate/.local:rw healthcheck: test: ["CMD-SHELL", "./venv/bin/python scripts/healthcheck.py"] python-runner: image: python:3.11-slim container_name: python-runner profiles: ["python-runner"] restart: unless-stopped # init + exec make SIGTERM reach python so the container stops fast init: true working_dir: /app command: /bin/sh -c 'if [ -f /app/requirements.txt ]; then python -m pip install --no-cache-dir -r /app/requirements.txt; fi; exec python /app/main.py' volumes: - ./python-runner:/app paddleocr: image: paddlepaddle/paddle:3.1.0 container_name: paddleocr profiles: ["paddleocr"] restart: unless-stopped # python -m paddlex --get_pipeline_config PP-ChatOCRv4-doc --save_path /app; \ command: /bin/sh -c "set -e; \ python -m pip install --upgrade pip; \ python -m pip install --no-cache-dir 'paddlex[all,ocr,ie]'; \ python -m paddlex --install serving; \ exec python -m paddlex --serve --pipeline '/app/ocr_config.yml' --device cpu --port 8080" volumes: - ./paddlex/ocr_config.yml:/app/ocr_config.yml:ro - paddleocr_cache:/root/.paddleocr - paddle_cache:/root/.cache/paddle - paddlex_data:/root/.paddlex healthcheck: test: ["CMD-SHELL", "wget -qO- http://localhost:8080/health > /dev/null || exit 1"] interval: 30s timeout: 10s retries: 5 waha: image: devlikeapro/waha:latest container_name: waha profiles: ["waha"] restart: unless-stopped environment: REDIS_URL: redis://redis:6379 WAHA_API_KEY: "${WAHA_API_KEY}" WAHA_BASE_URL: http://waha:3000 WAHA_DASHBOARD_ENABLED: "${WAHA_DASHBOARD_ENABLED:-true}" WAHA_DASHBOARD_PASSWORD: "${WAHA_DASHBOARD_PASSWORD}" WAHA_DASHBOARD_USERNAME: "${WAHA_DASHBOARD_USERNAME}" WAHA_ENGINE: "${WAHA_ENGINE}" WHATSAPP_SESSIONS_POSTGRESQL_URL: "postgres://postgres:${POSTGRES_PASSWORD}@postgres:5432/${WAHA_DB_NAME:-waha}?sslmode=disable" WHATSAPP_SWAGGER_ENABLED: "${WHATSAPP_SWAGGER_ENABLED:-true}" WHATSAPP_SWAGGER_PASSWORD: "${WHATSAPP_SWAGGER_PASSWORD}" WHATSAPP_SWAGGER_USERNAME: "${WHATSAPP_SWAGGER_USERNAME}" depends_on: redis: condition: service_healthy postgres: condition: service_healthy ragflow: # Pinned: 'latest' moved to 1.0 release candidates, which need kvrocks (and NATS) # the stack does not provide. Keep the tag in sync with the update preview. image: infiniflow/ragflow:v0.27.2 container_name: ragflow profiles: ["ragflow"] restart: unless-stopped environment: <<: *proxy-env DOC_ENGINE: elasticsearch ELASTIC_PASSWORD: "${RAGFLOW_ELASTICSEARCH_PASSWORD}" ES_HOST: ragflow-elasticsearch ES_PORT: 9200 MINIO_BUCKET: ragflow MINIO_HOST: ragflow-minio MINIO_PASSWORD: "${RAGFLOW_MINIO_ROOT_PASSWORD}" MINIO_PORT: 9000 MINIO_USER: minio MYSQL_DATABASE: rag_flow MYSQL_HOST: ragflow-mysql MYSQL_PASSWORD: "${RAGFLOW_MYSQL_ROOT_PASSWORD}" MYSQL_PORT: 3306 MYSQL_USER: root REDIS_HOST: ragflow-redis REDIS_PASSWORD: "${RAGFLOW_REDIS_PASSWORD}" REDIS_PORT: 6379 SVR_HTTP_PORT: 9380 depends_on: ragflow-elasticsearch: condition: service_healthy ragflow-mysql: condition: service_healthy ragflow-minio: condition: service_healthy ragflow-redis: condition: service_healthy ragflow-mysql: image: mysql:8 container_name: ragflow-mysql profiles: ["ragflow"] restart: unless-stopped environment: MYSQL_ROOT_PASSWORD: "${RAGFLOW_MYSQL_ROOT_PASSWORD}" MYSQL_DATABASE: rag_flow healthcheck: test: ["CMD-SHELL", "mysqladmin ping -h localhost || exit 1"] interval: 10s timeout: 5s retries: 10 start_period: 30s volumes: - ragflow_mysql_data:/var/lib/mysql ragflow-minio: # Silo (MinIO fork), see the 'minio' service above image: docker.io/pgsty/silo:latest container_name: ragflow-minio profiles: ["ragflow"] restart: unless-stopped entrypoint: sh command: -c 'mkdir -p /data/ragflow && silo server --address ":9000" --console-address ":9001" /data' environment: MINIO_ROOT_USER: minio MINIO_ROOT_PASSWORD: "${RAGFLOW_MINIO_ROOT_PASSWORD}" healthcheck: test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/cluster"] interval: 5s timeout: 5s retries: 10 start_period: 10s volumes: - ragflow_minio_data:/data ragflow-redis: image: docker.io/valkey/valkey:8-alpine container_name: ragflow-redis profiles: ["ragflow"] restart: unless-stopped command: "valkey-server --save 30 1 --loglevel warning --requirepass ${RAGFLOW_REDIS_PASSWORD}" volumes: - ragflow_redis_data:/data cap_drop: - ALL cap_add: - SETGID - SETUID - DAC_OVERRIDE logging: *default-logging healthcheck: test: ["CMD", "valkey-cli", "-a", "${RAGFLOW_REDIS_PASSWORD}", "ping"] interval: 3s timeout: 10s retries: 10 ragflow-elasticsearch: image: elasticsearch:8.11.3 container_name: ragflow-elasticsearch profiles: ["ragflow"] restart: unless-stopped environment: node.name: ragflow-es01 discovery.type: single-node ELASTIC_PASSWORD: "${RAGFLOW_ELASTICSEARCH_PASSWORD}" bootstrap.memory_lock: false xpack.security.enabled: true xpack.security.http.ssl.enabled: false xpack.security.transport.ssl.enabled: false cluster.routing.allocation.disk.watermark.low: 5gb cluster.routing.allocation.disk.watermark.high: 3gb cluster.routing.allocation.disk.watermark.flood_stage: 2gb ulimits: memlock: soft: -1 hard: -1 nofile: soft: 65536 hard: 65536 volumes: - ragflow_elasticsearch_data:/usr/share/elasticsearch/data healthcheck: test: [ "CMD-SHELL", "curl -u elastic:${RAGFLOW_ELASTICSEARCH_PASSWORD} http://localhost:9200/_cluster/health || exit 1", ] interval: 10s timeout: 10s retries: 120 start_period: 30s deploy: resources: limits: memory: 8g lightrag: image: ghcr.io/hkuds/lightrag:latest container_name: lightrag profiles: ["lightrag"] restart: unless-stopped depends_on: postgres: condition: service_healthy environment: <<: *proxy-env # Server Configuration HOST: 0.0.0.0 PORT: 9621 WEBUI_TITLE: LightRAG Knowledge Graph WEBUI_DESCRIPTION: Graph-based RAG with Knowledge Extraction # Authentication (Built-in) AUTH_ACCOUNTS: "${LIGHTRAG_USERNAME}:${LIGHTRAG_PASSWORD}" LIGHTRAG_API_KEY: "${LIGHTRAG_API_KEY}" TOKEN_SECRET: "${LIGHTRAG_TOKEN_SECRET}" # LLM Configuration (Ollama) LLM_BINDING: ollama LLM_MODEL: qwen2.5:32b LLM_BINDING_HOST: http://ollama:11434 OLLAMA_LLM_NUM_CTX: 32768 MAX_ASYNC: 4 # Embedding Configuration (Ollama) EMBEDDING_BINDING: ollama EMBEDDING_MODEL: bge-m3:latest EMBEDDING_DIM: 1024 EMBEDDING_BINDING_HOST: http://ollama:11434 OLLAMA_EMBEDDING_NUM_CTX: 8192 EMBEDDING_FUNC_MAX_ASYNC: 16 EMBEDDING_BATCH_NUM: 32 # Query Configuration ENABLE_LLM_CACHE: true TOP_K: 60 CHUNK_TOP_K: 20 COSINE_THRESHOLD: 0.2 MAX_ENTITY_TOKENS: 6000 MAX_RELATION_TOKENS: 8000 MAX_TOTAL_TOKENS: 30000 # Document Processing ENABLE_LLM_CACHE_FOR_EXTRACT: true SUMMARY_LANGUAGE: English CHUNK_SIZE: 1200 CHUNK_OVERLAP_SIZE: 100 SUMMARY_MAX_TOKENS: 500 SUMMARY_CONTEXT_SIZE: 10000 # Storage Configuration (Flexible - uses PostgreSQL/Neo4j if available) LIGHTRAG_KV_STORAGE: JsonKVStorage LIGHTRAG_DOC_STATUS_STORAGE: JsonDocStatusStorage LIGHTRAG_GRAPH_STORAGE: "${LIGHTRAG_GRAPH_STORAGE:-NetworkXStorage}" LIGHTRAG_VECTOR_STORAGE: "${LIGHTRAG_VECTOR_STORAGE:-NanoVectorDBStorage}" # PostgreSQL Configuration (if using PostgreSQL storage) POSTGRES_HOST: postgres POSTGRES_PORT: 5432 POSTGRES_USER: postgres POSTGRES_PASSWORD: "${POSTGRES_PASSWORD}" POSTGRES_DATABASE: "${LIGHTRAG_DB_NAME:-lightrag}" POSTGRES_MAX_CONNECTIONS: 12 POSTGRES_VECTOR_INDEX_TYPE: HNSW POSTGRES_HNSW_M: 16 POSTGRES_HNSW_EF: 200 # Neo4j Configuration (if using Neo4j storage) NEO4J_URI: bolt://neo4j:7687 NEO4J_USERNAME: "${NEO4J_AUTH_USERNAME:-neo4j}" NEO4J_PASSWORD: "${NEO4J_AUTH_PASSWORD}" NEO4J_DATABASE: neo4j # Directories INPUT_DIR: /app/data/inputs WORKING_DIR: /app/data/rag_storage volumes: - lightrag_data:/app/data/rag_storage - lightrag_inputs:/app/data/inputs extra_hosts: - "host.docker.internal:host-gateway" healthcheck: test: [ "CMD-SHELL", "http_proxy= https_proxy= HTTP_PROXY= HTTPS_PROXY= python -c \"import urllib.request; urllib.request.urlopen('http://localhost:9621/health', timeout=5)\" > /dev/null 2>&1 || exit 1", ] interval: 30s timeout: 10s retries: 5 start_period: 60s docling: image: "${DOCLING_IMAGE:-ghcr.io/docling-project/docling-serve-cpu}" container_name: docling profiles: ["docling"] restart: unless-stopped environment: <<: *proxy-env DOCLING_SERVE_ENABLE_UI: 1 DOCLING_SERVE_ENABLE_REMOTE_SERVICES: "${DOCLING_SERVE_ENABLE_REMOTE_SERVICES:-true}" DOCLING_SERVE_LOAD_MODELS_AT_BOOT: "${DOCLING_SERVE_LOAD_MODELS_AT_BOOT:-false}" DOCLING_DEVICE: "${DOCLING_DEVICE:-cpu}" volumes: - docling_cache:/opt/app-root/src/.cache shm_size: 1g healthcheck: test: [ "CMD-SHELL", "http_proxy= https_proxy= HTTP_PROXY= HTTPS_PROXY= wget -qO- http://localhost:5001/docs > /dev/null || exit 1", ] interval: 30s timeout: 10s retries: 5 start_period: 30s uptime-kuma: image: louislam/uptime-kuma:2 container_name: uptime-kuma profiles: ["uptime-kuma"] restart: unless-stopped logging: *default-logging environment: <<: *proxy-env UPTIME_KUMA_WS_ORIGIN_CHECK: bypass volumes: - uptime_kuma_data:/app/data healthcheck: test: ["CMD-SHELL", "node -e \"const http=require('http');const r=http.get('http://localhost:3001',res=>{process.exit(res.statusCode<400?0:1)});r.on('error',()=>process.exit(1));r.setTimeout(5000,()=>{r.destroy();process.exit(1)})\""] interval: 30s timeout: 10s retries: 5 start_period: 30s