name: ${AXON_COMPOSE_PROJECT_NAME:-axon} # Full axon stack: axon (HTTP), tei, chrome, and a mandatory bundled qdrant. # All host-published application/provider ports are loopback-only. Production # images are tag-plus-digest pinned; review and update both when upgrading. # service — qdrant is NOT optional in this file; every `up -d` starts all 4. # docker compose --env-file ~/.axon/.env -f docker-compose.prod.yaml up -d # For this deployment's specific override (qdrant lives on tootie for RAM # reasons, not devhost), apply docker-compose.external-qdrant.yaml on top: # docker compose --env-file ~/.axon/.env \ # -f docker-compose.prod.yaml -f docker-compose.external-qdrant.yaml up -d # That override skips starting the bundled axon-qdrant container and sets # QDRANT_URL directly from AXON_EXTERNAL_QDRANT_URL instead (overriding this # file's own AXON_QDRANT_URL-based default) — see that file's comments. # ~/.axon is the canonical host appdata root. Override AXON_HOME only when # relocating the full Axon appdata tree to another absolute path. x-common-service: &common-service networks: - axon restart: unless-stopped extra_hosts: - "host.docker.internal:host-gateway" ulimits: nofile: soft: 65535 hard: 65535 logging: driver: "json-file" options: max-size: "10m" max-file: "3" compress: "true" x-gpu-service: &gpu-service deploy: resources: reservations: devices: - driver: nvidia count: 1 capabilities: [gpu] services: axon: <<: *common-service image: ${AXON_IMAGE:-ghcr.io/dinglebear-ai/axon:latest@sha256:611e79987bd056b99646da5c4297540840e5dca774fbbbe1fefa244129d95476} container_name: ${AXON_CONTAINER_NAME:-axon} user: "1000:1000" env_file: - path: ${AXON_HOME:-${HOME}/.axon}/.env required: false environment: # Host-bleed guards: container user is `axon` — override host paths. # The shared ~/.axon/.env has these set to the host's $HOME, which # doesn't exist inside the container. env_file values override Dockerfile # ENV, so we must re-override here. AXON_HOME: /home/axon/.axon AXON_DATA_DIR: /home/axon/.axon # Pin service URLs so the axon container does not inherit host-loopback # values from ~/.axon/.env. Bundled qdrant is the default (mandatory, # always started below); docker-compose.external-qdrant.yaml overrides # this to point at an external instance (e.g. tootie) instead. QDRANT_URL: ${AXON_QDRANT_URL:-http://axon-qdrant:6333} TEI_URL: http://axon-tei:80 AXON_CHROME_REMOTE_URL: http://axon-chrome:6000 # AXON_SERVER_URL is a client-side switch. The shared ~/.axon/.env may # set it for host CLI commands, but the server container must not inherit # it or container-local maintenance commands will route back through HTTP. AXON_SERVER_URL: "" # Host-only bootstrap overrides must not leak from the shared env file # into the container and change config/env loading paths. AXON_ENV_FILE: "" AXON_CONFIG_PATH: "" AXON_CODEX_CMD: "" # Leave AXON_CODEX_HOME empty so axon's source-home resolver falls through # to CODEX_HOME below (graceful when ~/.axon/codex isn't seeded yet). AXON_CODEX_HOME: "" # Persist the container's codex home INSIDE the already-mounted ~/.axon, so # config.toml (MCP servers), auth.json, refreshed OAuth tokens, and sessions # survive container recreates. Fresh/empty by default → fast init, and fully # separate from the operator's host ~/.codex. Seed it once on the host: # CODEX_HOME=~/.axon/codex codex login # (or: mkdir -p ~/.axon/codex && cp ~/.codex/auth.json ~/.axon/codex/) # API-key auth (OPENAI_API_KEY in ~/.axon/.env) works without seeding. CODEX_HOME: /home/axon/.axon/codex TZ: ${TZ:-America/New_York} # Filesystem-namespace claim affinity (axon_rust-p2oc). Embed jobs whose # input is a local path are stamped with the enqueueing process's namespace # at insert time, then only claimed by workers in the same namespace. The # container uses "axon-container"; host CLI workers leave this unset (or # set it explicitly) so they only claim namespace-NULL (URL/text) jobs and # their own local-path jobs — never container-path jobs. AXON_FS_NAMESPACE: axon-container volumes: # Persist config + secrets + jobs.db + output + logs + artifacts on the host. - ${AXON_HOME:-${HOME}/.axon}:/home/axon/.axon # Persist OAuth client registrations, refresh tokens, and JWT signing key # across container recreates so upstream gateways don't hold stale client IDs. - ${AXON_HOME:-${HOME}/.axon}/lab-auth:/home/axon/.lab # Gemini CLI auth files — headless ask/evaluate/research synthesis reads # oauth_creds.json, gemini-credentials.json, google_accounts.json from here. # Prerequisite: run `gemini auth login` on the host before starting the container # so ${HOME}/.gemini exists and contains valid credentials. If the directory is # absent, Docker will create it as root-owned — run `mkdir -p ~/.gemini` first. - ${GEMINI_HOME:-${HOME}/.gemini}:/home/axon/.gemini:ro # NOTE: codex auth/config is NOT a separate mount — the container's codex # home is CODEX_HOME=/home/axon/.axon/codex (see environment above), which # already persists via the ~/.axon bind mount on the first volume line. ports: - "127.0.0.1:${AXON_HTTP_PUBLISH:-8001}:8001" command: ["serve"] depends_on: axon-tei: condition: service_healthy axon-chrome: condition: service_healthy healthcheck: test: ["CMD-SHELL", "curl -fsS --max-time 4 http://127.0.0.1:8001/healthz >/dev/null || exit 1"] interval: 30s timeout: 5s retries: 3 start_period: 180s axon-qdrant: <<: *common-service image: qdrant/qdrant:v1.18.2@sha256:75eab8c4ba42096724fdcfde8b4de0b5713d529dde32f285a1f86fdcb2c9e50c container_name: ${AXON_QDRANT_CONTAINER_NAME:-axon-qdrant} # Shield the vector DB from the kernel's *global* OOM killer: under host # memory pressure (concurrent cargo builds + optimizer churn) qdrant is the # largest RSS on the box and got picked twice in 10 minutes (dmesg # CONSTRAINT_NONE — host-level, not this container's cgroup cap). Prefer # killing a rebuildable build process over the database. oom_score_adj: -500 ports: - "127.0.0.1:${QDRANT_HTTP_PORT:-53333}:6333" - "127.0.0.1:${QDRANT_GRPC_PORT:-53334}:6334" # gRPC volumes: - ${AXON_HOME:-${HOME}/.axon}/qdrant:/qdrant/storage - ./config/qdrant/production.yaml:/qdrant/config/production.yaml:ro deploy: resources: limits: # axon collection's resident working set (int8 quant always_ram + in-RAM # HNSW index) stably sits ~12.5G and was OOM-killed against a 12G cap # (206 restarts, dmesg CONSTRAINT_MEMCG). 16G gives ~28% headroom over # the stable set. To reduce real RAM instead, flip the axon collection's # quantization always_ram:false + hnsw on_disk:true (trades query latency). memory: 16G reservations: memory: 1G healthcheck: test: [ "CMD", "/usr/bin/bash", "-ec", "exec 3<>/dev/tcp/127.0.0.1/6333; printf \"GET /readyz HTTP/1.1\\r\\nHost: 127.0.0.1\\r\\nConnection: close\\r\\n\\r\\n\" >&3; head -n 1 <&3 | grep -q \"200\"", ] interval: 10s timeout: 5s retries: 12 start_period: 120s axon-tei: <<: [*common-service, *gpu-service] image: ghcr.io/huggingface/text-embeddings-inference:89-1.9@sha256:e47e625ced2385d3dbfdee79ba0380204578e0b27ef1a926783f9b3486aaf109 container_name: ${AXON_TEI_CONTAINER_NAME:-axon-tei} environment: # Container-internal path — not suitable for shared .env. HF_HUB_CACHE: /data/cache # TEI-specific filter — would suppress axon's logs if in shared .env. RUST_LOG: text_embeddings_router=info ports: - "127.0.0.1:${TEI_HTTP_PORT:-52000}:80" volumes: - ${AXON_HOME:-${HOME}/.axon}/tei:/data # TEI server-side tuning below is compose-only interpolation — these vars # have no config.toml home because they configure the TEI *container* # process, not axon itself. Set them in ~/.axon/.env only if you need to # override the inline defaults; do not add more axon tuning vars here. # TEI_MAX_CONCURRENT_REQUESTS (default 512) # TEI_MAX_BATCH_TOKENS (default 196608) # TEI_MAX_BATCH_REQUESTS (default 512) # TEI_SERVER_MAX_CLIENT_BATCH_SIZE (default 128) # TEI_POOLING (default last-token) # TEI_TOKENIZATION_WORKERS (default 20) command: - --model-id - ${TEI_EMBEDDING_MODEL:-Qwen/Qwen3-Embedding-0.6B} - --dtype - float16 - --max-concurrent-requests - "${TEI_MAX_CONCURRENT_REQUESTS:-512}" - --max-batch-tokens - "${TEI_MAX_BATCH_TOKENS:-196608}" - --max-batch-requests - "${TEI_MAX_BATCH_REQUESTS:-512}" - --max-client-batch-size - "${TEI_SERVER_MAX_CLIENT_BATCH_SIZE:-128}" - --pooling - ${TEI_POOLING:-last-token} - --tokenization-workers - "${TEI_TOKENIZATION_WORKERS:-20}" - --auto-truncate healthcheck: test: ["CMD", "curl", "-f", "http://localhost:80/health"] interval: 30s timeout: 10s retries: 3 start_period: 20s axon-chrome: # headless_browser manager + chrome-headless-shell # Port 6000: management API (GET / → "healthy", /fork, /shutdown) # Port 9222: CDP proxy — pointed at by AXON_CHROME_REMOTE_URL # Security: no built-in auth on CDP/headless_browser. Bind the bundled # provider to host loopback only; remote deployments must use an explicit # authenticated tunnel/proxy or the external-provider overlay. <<: *common-service container_name: ${AXON_CHROME_CONTAINER_NAME:-axon-chrome} build: context: ./config dockerfile: chrome/Dockerfile shm_size: "2gb" ports: - "127.0.0.1:${AXON_CHROME_CDP_PORT:-9222}:9222" # CDP proxy - "127.0.0.1:${AXON_CHROME_DEVTOOLS_PORT:-9223}:9223" # raw Chrome DevTools - "127.0.0.1:${AXON_CHROME_MANAGEMENT_PORT:-6000}:6000" # management API environment: HOSTNAME_OVERRIDE: axon-chrome healthcheck: test: ["CMD-SHELL", "curl -fsSL --max-time 4 http://127.0.0.1:9222/json/version > /dev/null"] interval: 10s timeout: 5s retries: 6 start_period: 20s networks: axon: name: ${DOCKER_NETWORK:-axon} external: true