# compose.all-in-one.yml — single-host deployment (xiaozhi-server) # # Since the #36 cutover (2026-05-19) the Dotty stack is several containers, # all on one Docker host: # # - xiaozhi-esp32-server — voice I/O ← this file # - dotty-pi — the pi agent brain ← dotty-pi/docker-compose.yml # - dotty-behaviour — perception+greeter ← dotty-behaviour/docker-compose.yml # - bridge.py — the admin dashboard (port 8081) # # This file brings up xiaozhi-esp32-server only. Bring up the brain and # behaviour containers from their own compose files — see dotty-pi/README.md # and dotty-behaviour/README.md. docker-compose.yml.template is the canonical # definition with the full annotations and the optional NVIDIA/CUDA block; this # file is the single-host convenience copy and MUST stay at volume/env parity # with it. The default PiVoiceLLM voice path and the perception relay depend on # the docker-socket mount and the xiaozhi-patch volumes below — omitting them # (as an earlier revision did) yields a container that boots but cannot reach # the brain and drops every perception event. # # ─── Prerequisites ────────────────────────────────────────────────────── # # 1. The `dotty-pi` container must be running — the default LLM provider # (PiVoiceLLM) reaches it via `docker exec`. See dotty-pi/README.md. # That also requires the host docker socket mounted into this container # (see docker-compose.yml.template). # 2. ASR model weights must be downloaded into models/SenseVoiceSmall/. # See the main README for setup instructions. # 3. If using LocalPiper TTS, download Piper voice files into models/piper/. # # ─── Quick start ──────────────────────────────────────────────────────── # # 1. Copy .env.example to .env and fill in real values. # 2. Edit data/.config.yaml — replace with this host's LAN IP # (the StackChan device needs a LAN-reachable IP for WebSocket/OTA). # 3. docker compose -f compose.all-in-one.yml up -d # 4. Point StackChan OTA URL to http://:8003/xiaozhi/ota/ # # ─── What to change for your environment ──────────────────────────────── # # - TZ: set to your IANA timezone (e.g. America/New_York, Europe/London) # - Piper model paths in data/.config.yaml if you use different voices # services: # ── xiaozhi-esp32-server ───────────────────────────────────────────── # Voice pipeline: ASR, TTS, emotion parsing, WebSocket to the StackChan. # The default LLM provider (PiVoiceLLM) reaches the `dotty-pi` container # via `docker exec` — see docker-compose.yml.template for the docker-socket # mount that requires. xiaozhi-server: build: context: . dockerfile: Dockerfile image: xiaozhi-esp32-server-piper:local container_name: xiaozhi-esp32-server restart: unless-stopped environment: # EDIT: set to your local timezone - TZ=Australia/Brisbane # EDIT: dotty-behaviour service URL — the target for vision intent AND the # perception-event relay. Same host; use the host LAN IP, not loopback. # Without it the relay logs "dropping perception event" and the whole # dotty-behaviour consumer layer goes inert. - VISION_BRIDGE_URL=http://:8090 # Active persona file under ./personas/ (loaded by the LLM provider). - PERSONA=default # kid_mode / smart_mode toggle state — receiveAudioHandle.py READS these to # paint the firmware LED pips on reconnect; the bridge dashboard WRITES # them. Both containers MUST point at the same files (bind-mounted below, # read-only). Without these the reader falls through to a dead default and # the firmware pips desync from the dashboard on every reconnect. - DOTTY_KID_MODE_STATE=/var/lib/dotty-bridge/state/kid-mode - DOTTY_SMART_MODE_STATE=/var/lib/dotty-bridge/state/smart-mode # Shared admin-API secret (X-Admin-Token). Read from ./.env — `make # setup` generates it. Unset/empty = permissive (admin routes open); # set = this server enforces it AND every caller (bridge, # dotty-behaviour, dotty-pi) must carry the same value in its own # .env. See .env.example. - DOTTY_ADMIN_TOKEN=${DOTTY_ADMIN_TOKEN:-} ports: # 8000: WebSocket — StackChan connects here - "8000:8000" # 8003: HTTP — OTA endpoint for StackChan firmware config - "8003:8003" volumes: - ./data/.config.yaml:/opt/xiaozhi-esp32-server/data/.config.yaml:ro - ./models/SenseVoiceSmall:/opt/xiaozhi-esp32-server/models/SenseVoiceSmall - ./tmp:/opt/xiaozhi-esp32-server/tmp - ./custom-providers/openai_compat:/opt/xiaozhi-esp32-server/core/providers/llm/openai_compat - ./custom-providers/pi_voice:/opt/xiaozhi-esp32-server/core/providers/llm/pi_voice # PiVoiceLLM (the default LLM) shells out to `docker exec -i dotty-pi pi`, # so this container needs the docker CLI binary AND the host docker socket. # WARNING: mounting /var/run/docker.sock gives this container effective # root on the docker host. Acceptable for a single-purpose self-hosted # appliance; do not enable on a multi-tenant host. Comment both lines out # if you switch `selected_module.LLM` to OpenAICompat. - /var/run/docker.sock:/var/run/docker.sock - /usr/bin/docker:/usr/bin/docker:ro - ./personas:/opt/xiaozhi-esp32-server/personas:ro - ./custom-providers/edge_stream/edge_stream.py:/opt/xiaozhi-esp32-server/core/providers/tts/edge_stream.py:ro - ./custom-providers/piper_local/piper_local.py:/opt/xiaozhi-esp32-server/core/providers/tts/piper_local.py:ro - ./models/piper:/opt/xiaozhi-esp32-server/models/piper:ro - ./custom-providers/asr/fun_local.py:/opt/xiaozhi-esp32-server/core/providers/asr/fun_local.py:ro - ./custom-providers/asr/whisper_local.py:/opt/xiaozhi-esp32-server/core/providers/asr/whisper_local.py:ro - ./models/whisper-small.en-ct2:/opt/xiaozhi-esp32-server/models/whisper-small.en-ct2:ro - ./receiveAudioHandle.py:/opt/xiaozhi-esp32-server/core/handle/receiveAudioHandle.py:ro - ./dances.py:/opt/xiaozhi-esp32-server/core/handle/dances.py:ro - ./custom-providers/textUtils.py:/opt/xiaozhi-esp32-server/core/utils/textUtils.py:ro # DOTTY DeviceCommand seam: MCP request ids + envelope + per-conn # serialized sends, shared by http_server.py + receiveAudioHandle.py - ./custom-providers/xiaozhi-patches/device_command.py:/opt/xiaozhi-esp32-server/core/utils/device_command.py:ro - ./songs:/opt/xiaozhi-esp32-server/config/assets/songs:ro # DOTTY portal + admin routes + active-connection registry - ./custom-providers/xiaozhi-patches/portal_bridge.py:/opt/xiaozhi-esp32-server/core/portal_bridge.py:ro - ./custom-providers/xiaozhi-patches/websocket_server.py:/opt/xiaozhi-esp32-server/core/websocket_server.py:ro - ./custom-providers/xiaozhi-patches/http_server.py:/opt/xiaozhi-esp32-server/core/http_server.py:ro # DOTTY ambient perception: relay firmware "event" frames to dotty-behaviour - ./custom-providers/xiaozhi-patches/textMessageHandlerRegistry.py:/opt/xiaozhi-esp32-server/core/handle/textMessageHandlerRegistry.py:ro - ./custom-providers/asr/sensevoice_onnx.py:/opt/xiaozhi-esp32-server/core/providers/asr/sensevoice_onnx.py:ro - ./models/SenseVoiceSmall-onnx:/opt/xiaozhi-esp32-server/models/SenseVoiceSmall-onnx:ro # OTA: patched ota_handler that composes firmware download URLs directly # (upstream's vision_url.replace path was a no-op when vision_explain # doesn't contain "/mcp/vision/explain"). See docs/ota-verification.md. - ./custom-providers/xiaozhi-patches/ota_handler.py:/opt/xiaozhi-esp32-server/core/api/ota_handler.py:ro - ./data/bin:/opt/xiaozhi-esp32-server/data/bin # Shared kid/smart-mode toggle state (READ-ONLY here — the bridge dashboard # owns writes). Host path MUST match the bridge container's state mount. # Override DOTTY_STATE_DIR if your bridge writes elsewhere. - ${DOTTY_STATE_DIR:-/mnt/user/appdata/dotty-bridge/state}:/var/lib/dotty-bridge/state:ro