services: otari: # Pulls from Docker Hub by default. Contributors testing a feature branch # with unreleased code should build the image locally first: # docker build -t mzdotai/otari:latest . # Docker prefers the local image over the registry tag once it exists. image: docker.io/mzdotai/otari:latest # If you want to use a different port, change it here and in the config.yml file. ports: # Host port 8000 by default. Override via $OTARI_PORT when other # services on the host already own :8000 (e.g. another local backend). - "${OTARI_PORT:-8000}:8000" volumes: # Default `./config.yml` matches the existing operator convention # (gitignored, operator-supplied). The demo overrides this via # OTARI_CONFIG_PATH=./demo/code-exec/otari-config.yml so the # repo's main config.yml stays untouched. - ${OTARI_CONFIG_PATH:-./config.yml}:/app/config.yml - ./service_account.json:/app/service_account.json command: ["otari", "serve", "--config", "/app/config.yml"] environment: # In-network URL of the sandbox image — set so Otari can dispatch # `code_execution` tool calls to the sandbox container without any # client-side configuration. - OTARI_SANDBOX_URL=http://sandbox:8080 # Optional operator-level purpose hint for the code_execution tool. Falls # back to a Python/pandas/numpy-flavoured default if unset. Per-request # tools[i].purpose_hint still wins over this. - OTARI_SANDBOX_PURPOSE_HINT=${OTARI_SANDBOX_PURPOSE_HINT:-} # Managed Web Fetch is opt-in because it permits model-directed outbound # requests to public web destinations. An unset override defers to config.yml. - OTARI_WEB_FETCH_ENABLED=${OTARI_WEB_FETCH_ENABLED:-} # In-network URL of the web-search backend — any HTTP service exposing # a SearXNG-compatible /search?format=json endpoint. The bundled # `searxng` profile container is the default; swap by changing this URL # to point at any other backend. - OTARI_WEB_SEARCH_URL=${OTARI_WEB_SEARCH_URL:-http://searxng:8080} # A licensed search API Otari calls itself, which needs no container and # wins over the URL above when both are set. `tavily` or `brave`; the key # stays in this process and is never handed to a caller. - OTARI_WEB_SEARCH_PROVIDER=${OTARI_WEB_SEARCH_PROVIDER:-} - OTARI_WEB_SEARCH_PROVIDER_API_KEY=${OTARI_WEB_SEARCH_PROVIDER_API_KEY:-} # Optional knobs. Defaults are conservative — see web_search_backend.py. - OTARI_WEB_SEARCH_ENGINES=${OTARI_WEB_SEARCH_ENGINES:-} - OTARI_WEB_SEARCH_MAX_RESULTS=${OTARI_WEB_SEARCH_MAX_RESULTS:-} - OTARI_WEB_SEARCH_EXTRACT=${OTARI_WEB_SEARCH_EXTRACT:-} - OTARI_WEB_SEARCH_PURPOSE_HINT=${OTARI_WEB_SEARCH_PURPOSE_HINT:-} # Run a provider-named web-search declaration (bare `web_search`, # `web_search_`) against the backend above instead of forwarding it, # so a client that only speaks a provider's vocabulary (Claude Code, the # Anthropic SDK) reaches it. Off unless set — see docs/tools.md. - OTARI_WEB_SEARCH_INTERCEPT=${OTARI_WEB_SEARCH_INTERCEPT:-} # In-network URL of the guardrails service — the otari-anyguardrails # container, which exposes POST /validate over `any-guardrail`. Callers # opt a request in per-request via the top-level `guardrails` field; the # Otari runs the configured profile(s) against this URL before calling # the provider. Brought up by the `guardrails` compose profile below. - OTARI_GUARDRAILS_URL=${OTARI_GUARDRAILS_URL:-http://anyguardrails:8000} # Optional operator-level header for the per-tool hint block in the system # message. Falls back to "You have access to the following tools:" if unset. # Useful for tone/translation/deployment-wide preferences. Not per-request: # callers who want a custom framing should set their own system message. - OTARI_TOOLS_HEADER=${OTARI_TOOLS_HEADER:-} # Pass provider API keys from host env so config.yml stays free of # secrets: reference them there as `api_key: ${OPENAI_API_KEY}` under # the provider's `providers:` entry. - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY:-} - OPENAI_API_KEY=${OPENAI_API_KEY:-} # The same for search providers, read under `search_tools:` as # `api_key: ${EXA_API_KEY}`. - EXA_API_KEY=${EXA_API_KEY:-} - MOJEEK_API_KEY=${MOJEEK_API_KEY:-} # And for fetch providers, under `fetch_tools:` as # `api_key: ${TABSTACK_API_KEY}`. - TABSTACK_API_KEY=${TABSTACK_API_KEY:-} # Deployment-wide management credential; overrides `master_key` in # config.yml. Unset, Otari generates one on first boot and prints it once. - OTARI_MASTER_KEY=${OTARI_MASTER_KEY:-} # Fernet key that encrypts provider credentials added through the admin # dashboard. Needed only for the dashboard-first flow (add providers in # the browser); unset is fine when providers live in config.yml. Generate # one with `docker run --rm mzdotai/otari:latest otari gen-secret-key`. - OTARI_SECRET_KEY=${OTARI_SECRET_KEY:-} # Key that names the provider account a copy of an attached file is in. # Required while provider copies are on, which they are by default, and # it must differ from OTARI_SECRET_KEY. Generate one with # `openssl rand -base64 32`. - OTARI_PROVIDER_ACCOUNT_PEPPER=${OTARI_PROVIDER_ACCOUNT_PEPPER:?set OTARI_PROVIDER_ACCOUNT_PEPPER, for example to the output of openssl rand -base64 32} # Local llamafile (open-weight model) endpoint. host.docker.internal # routes from Otari container back to a llamafile running on # the macOS/Windows host. Override if you run the model elsewhere. - LLAMAFILE_API_BASE=${LLAMAFILE_API_BASE:-http://host.docker.internal:8080/v1} # Credentials for the `s3` files backend, and for `fsspec` over s3://. # The seaweedfs service below uses the same pair. Bedrock also reads them # when it has none of its own. - AWS_ACCESS_KEY_ID=${OTARI_S3_ACCESS_KEY:-} - AWS_SECRET_ACCESS_KEY=${OTARI_S3_SECRET_KEY:-} networks: - default - object-storage depends_on: postgres: condition: service_healthy restart: unless-stopped postgres: image: postgres:16-alpine ports: # Bound to localhost only. The database holds API key hashes, users, # budgets, usage, and dashboard sessions, so an open port on the host # network would expose all of it (default creds otari/otari) to anyone # on the LAN. The Otari container reaches it via the Docker network at # postgres:5432; this host mapping is only for external tools (psql / # TablePlus / etc.) on the operator's own machine. # Host port 5433 (container still :5432) avoids clashing with another # local postgres on the default port. Override with $OTARI_POSTGRES_PORT. - "127.0.0.1:${OTARI_POSTGRES_PORT:-5433}:5432" environment: - POSTGRES_USER=otari - POSTGRES_PASSWORD=otari - POSTGRES_DB=otari volumes: - postgres_data:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U otari"] interval: 10s timeout: 5s retries: 5 restart: unless-stopped # Code-execution sandbox — a Python REPL container Otari dispatches # `code_execution` tool calls to. Source + build live in their own repo: # https://github.com/mozilla-ai/otari-sandbox-container — CI publishes # `mzdotai/otari-sandbox-container:{latest,}` on Docker Hub on every # push to that repo's main branch. # Override the tag via $OTARI_SANDBOX_IMAGE if you need a specific build. # Distinct from the gateway's own $OTARI_SANDBOX_SESSION_IMAGE, which names the # image a leased session runs rather than the container booted here. # # Opt-in via the `code-exec` compose profile so operators who don't use # code_execution aren't forced to pull this image or wait on its health # check. Bring it up with `docker compose --profile code-exec up` (the # demo's start.sh passes this flag automatically). When the sandbox is # not running, Otari returns a clean 502 to any request that # actually tries to dispatch `code_execution`. sandbox: profiles: ["code-exec"] image: ${OTARI_SANDBOX_IMAGE:-docker.io/mzdotai/otari-sandbox-container:latest} ports: # Bound to localhost only: the sandbox runs arbitrary user-supplied # code, so an open port on the host network would be an RCE surface # to anyone on the LAN. The Otari container reaches it via the # Docker network at `sandbox:8080`; this host-side mapping is just # for ad-hoc curl/debugging from the operator's own machine. - "127.0.0.1:8180:8080" environment: - SANDBOX_SESSIONS_ROOT=/var/sandbox/sessions - SANDBOX_SESSION_IDLE_TIMEOUT=900 - SANDBOX_SESSION_MAX_LIFETIME=7200 healthcheck: test: ["CMD", "curl", "-fsS", "http://localhost:8080/health"] interval: 10s timeout: 5s retries: 5 start_period: 5s restart: unless-stopped # Web-search backend — a SearXNG instance Otari dispatches `web_search` # tool calls to. SearXNG is a metasearch front-end over many upstream # engines; Otari's default engine list deliberately excludes Google, # Bing, Yahoo and Brave (which forbid automated querying or sell a paid # API) and uses duckduckgo/mojeek/qwant/wikipedia — the most defensible # OSS combination. Operators who enable scraping-of-major-engines do so # consciously and should review the upstream engines' Terms of Service. # # For commercial / production use, run a licensed API instead. Tavily and # Brave need no container at all: set OTARI_WEB_SEARCH_PROVIDER and # OTARI_WEB_SEARCH_PROVIDER_API_KEY and Otari calls them directly, leaving # OTARI_WEB_SEARCH_URL unset. Any other HTTP service exposing a # SearXNG-compatible /search?format=json shape is still a drop-in # replacement via OTARI_WEB_SEARCH_URL. # # Opt-in via the `web-search` compose profile so operators who don't use # the web_search tool aren't forced to pull this image. Bring it up with # `docker compose --profile web-search up`. When the searxng container is # not running, Otari returns a clean 502 to any request that tries # to dispatch `web_search`. searxng: profiles: ["web-search"] image: docker.io/searxng/searxng:latest ports: # Bound to localhost only — like sandbox, this is internal infra. The # Otari reaches it via the Docker network at `searxng:8080`; the # host mapping is just for ad-hoc curl/debugging. - "127.0.0.1:8181:8080" volumes: - ./scripts/searxng/settings.yml:/etc/searxng/settings.yml:ro environment: # Public-facing base URL SearXNG uses for self-references (OpenSearch # descriptor, UI links, CSP origins). Must match the host port mapping # above so ad-hoc curl / browser access lines up; Otari itself # reaches SearXNG over the Docker network at ``searxng:8080`` and # doesn't use this value. - SEARXNG_BASE_URL=http://localhost:8181/ # SearXNG uses a per-instance secret for CSRF tokens etc. The default # below is fine for a single-tenant Otari-internal deployment; rotate # via $SEARXNG_SECRET for multi-tenant or public-facing setups. - SEARXNG_SECRET=${SEARXNG_SECRET:-otari-local-dev-secret} restart: unless-stopped # Guardrails service — the otari-anyguardrails container, a stateless FastAPI # service that wraps `any-guardrail` (https://github.com/mozilla-ai/any-guardrail) # behind `POST /validate`. Otari calls it whenever a request carries the # top-level `guardrails` field (see services/guardrails.py). Source lives at # https://github.com/mozilla-ai/otari-anyguardrails-container. # # The published image is multi-arch (linux/amd64 + linux/arm64). Override the # tag via $OTARI_ANYGUARDRAILS_IMAGE. (If the pull 401s, the Docker Hub repo # may still be private — it must be public for external users.) # # Opt-in via the `guardrails` compose profile — which also includes the # `encoderfile` container below, so `docker compose --profile guardrails up` # brings up the whole PIGuard-via-encoderfile guardrail with no extra flags. # When this container is not running, Otari returns a clean 502 to any # request that uses `guardrails`. The mounted service config defaults to the # encoderfile profile; `./start.sh --in-process` swaps in the # InjecGuard/HuggingFace config and skips the encoderfile container. anyguardrails: profiles: ["guardrails"] image: ${OTARI_ANYGUARDRAILS_IMAGE:-docker.io/mzdotai/otari-any-guardrail-container:latest} ports: # Localhost-only, like the other internal backends. Otari reaches it # over the Docker network at `anyguardrails:8000`; this mapping is for # ad-hoc curl/debugging (e.g. GET /profiles, POST /validate). - "127.0.0.1:8183:8000" volumes: # Guardrail config. Defaults to the encoderfile (PIGuard) profile; override # with GUARDRAILS_CONFIG_PATH (start.sh --in-process points it at the # InjecGuard/HF config). Mount path matches ANY_GUARDRAILS_CONFIG_PATHS. - ${GUARDRAILS_CONFIG_PATH:-./demo/guardrails/guardrails-encoderfile-service.yaml}:/app/config/service.yaml:ro # Cache HuggingFace model downloads across restarts (the first /validate # for a HF-backed profile pulls the model). - anyguardrails_hf_cache:/app/hf_cache environment: - ANY_GUARDRAILS_CONFIG_PATHS=/app/config/service.yaml - HF_HUB_CACHE=/app/hf_cache # HuggingFace token — required for gated models; harmless if unset for # public ones. Passed from host env so it stays out of the config file. - HF_TOKEN=${HF_TOKEN:-} restart: unless-stopped # Encoderfile backend — a single self-contained binary (Mozilla `encoderfile`, # https://github.com/mozilla-ai/encoderfile) that serves a transformer encoder # over HTTP. The anyguardrails container's `prompt-injection` profile reaches # it via the EncoderfileProvider at http://encoderfile:8080 (see # demo/guardrails/guardrails-encoderfile-service.yaml). The published image # bakes in the PIGuard prompt-injection model. # # Part of the `guardrails` profile, so `docker compose --profile guardrails up` # brings up the full prompt-injection guardrail (anyguardrails + encoderfile) # with no extra flags. (`./start.sh --in-process` runs only Otari/postgres/ # anyguardrails with the HuggingFace config instead.) # # The encoderfile binary is architecture-specific, so the images are published # per-arch (the arch is in the tag name, e.g. `.aarch64-linux-gnu` / # `.x86_64-linux-gnu`) rather than as one multi-arch tag. The default below is # the arm64 build; on x86 set OTARI_ENCODERFILE_IMAGE to the `.x86_64-linux-gnu` # tag (the demo's start.sh picks the right one automatically via `uname -m`). encoderfile: profiles: ["guardrails"] image: ${OTARI_ENCODERFILE_IMAGE:-docker.io/mzdotai/piguard.aarch64-linux-gnu-encoderfile:v0.6.2} ports: # Localhost-only debug port; the anyguardrails container reaches it over # the Docker network at `encoderfile:8080`. - "127.0.0.1:8184:8080" restart: unless-stopped # SeaweedFS is an S3-compatible store for the `s3` and `fsspec` files backends. # Start it with `docker compose --profile object-storage up -d`. # docs/files.md has the settings that use it. seaweedfs: profiles: ["object-storage"] image: ${OTARI_SEAWEEDFS_IMAGE:-docker.io/chrislusf/seaweedfs:4.47} # These flags turn off the admin UI, which has no password, and the servers # Otari does not use. command: ["mini", "-dir=/data", "-admin.ui=false", "-webdav=false", "-s3.port.iceberg=0", "-s3.port.lance=0"] ports: # The port is bound to localhost only. Otari reaches the store over the # Docker network at `seaweedfs:8333`. - "127.0.0.1:${OTARI_S3_PORT:-8333}:8333" environment: # The defaults are for local use only and do not reach Otari. - AWS_ACCESS_KEY_ID=${OTARI_S3_ACCESS_KEY:-otari} - AWS_SECRET_ACCESS_KEY=${OTARI_S3_SECRET_KEY:-otari-s3-secret} - S3_BUCKET=otari-files # The filer API answers without credentials, so only otari joins this # network. Keep every other service off it. networks: - object-storage volumes: - seaweedfs_data:/data healthcheck: test: ["CMD", "curl", "-fsS", "http://localhost:8333/healthz"] interval: 10s timeout: 5s retries: 5 restart: unless-stopped volumes: postgres_data: anyguardrails_hf_cache: seaweedfs_data: networks: object-storage: