docker-compose.prod.yaml
Rama main · réplica de lectura del repositorio original.
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