# skaldsong stack — single FastAPI + SPA container. # # Skaldsong is the wizard + reader surface that talks to Worldtree as a # Bifrost consumer. One process serves both the SvelteKit-style SPA # (static, baked into the image) and the wizard backend (FastAPI), # plus the Bifrost endpoint Worldtree calls back to. # # Image is built + pushed by vh/skaldsong's `.gitea/workflows/deploy.yaml` # to gitea.phasefinal.com/vh/skaldsong, then pulled here. Workflow # passes the triggering commit SHA via `--var ref=...` to elway, which # substitutes it into ${SKALDSONG_IMAGE} for this compose. SHA-pinned # only by current preference — no :latest moving-tag advance (re-evaluate # once the /health endpoint exercises Worldtree + Kokoro reachability). # # Internal tooling — accessed directly on host:port over the LAN, does # NOT traverse Traefik. State persists under /opt/docker/conf/skaldsong/ # {db,runs} on the host; db/ holds skaldsong-ui.db (SQLite, wizard state # + story rows), runs/ holds per-story generation pipeline artifacts # (spec, state, output, logs, manuscripts). # # Per skaldsong's INV-006: process restart marks any 'running' generation # rows 'partial' and never auto-resumes — deploy mid-run is safe; in-flight # work looks to the SPA like a power outage. # # All tunables live in .env — edit that, not this file. services: skaldsong-host: image: ${SKALDSONG_IMAGE} container_name: skaldsong restart: unless-stopped ports: - "${SKALDSONG_BIND:-0.0.0.0}:${SKALDSONG_PORT}:8000" environment: &skaldsong-env # App always listens on 8000 internally; host port is the only knob. # Worldtree integration — JWT key is the shared-secret Bearer per # Worldtree's HS256 path (same value as Worldtree's own # WORLDTREE_SKALDSONG_USER_KEY). NEVER commit; .env on the server # holds the real value. SKALDSONG_BIFROST_JWT_KEY: "${SKALDSONG_BIFROST_JWT_KEY:-}" # URL Worldtree uses to call BACK to skaldsong's Bifrost endpoint # at tool-dispatch time. This is skaldsong's host-side reachable # URL from corviduo-dev's perspective — must match Worldtree's # BIFROST_CLIENT_ALLOWED_HOSTS entry. Coordinated allowlist update # required if host:port changes. SKALDSONG_HOST_BIFROST_ENDPOINT_URL: "${SKALDSONG_HOST_BIFROST_ENDPOINT_URL:-}" # Worldtree API client. WORLDTREE_TOKEN is the SAME shared-secret # Bearer as SKALDSONG_BIFROST_JWT_KEY above — different code path # consumes it (outbound HTTP Authorization header vs inbound HS256 # JWT verify). WORLDTREE_BASE_URL is corviduo-dev's IP:port # LAN-direct from ana-docker. WORLDTREE_TOKEN: "${WORLDTREE_TOKEN:-}" WORLDTREE_BASE_URL: "${WORLDTREE_BASE_URL:-http://10.250.50.152:8080}" # Wizard agent ID on Worldtree. MUST be pinned to the existing # agent slot (`skaldsong:wizard-v2` per skaldsong-dev's prior # hand-launched setup); leaving blank on first boot would POST # /agents/define and burn another slot of the 50-agent-per-key # Heimdall quota. Heimdall scopes agent_id to user_id, so the # ana-docker deployment inherits the same id as the nh3-dev # hand-launch. SKALDSONG_HOST_WIZARD_AGENT_ID: "${SKALDSONG_HOST_WIZARD_AGENT_ID:-skaldsong:wizard-v2}" # CORS for the SPA. Comma-separated origins; include both the # public hostname and any dev origins still in rotation. SKALDSONG_HOST_CORS_ORIGINS: "${SKALDSONG_HOST_CORS_ORIGINS:-}" # TTS — Kokoro on irv-ml1, reached over WireGuard via ana-wg from # ana-docker. Same URL works fleet-wide. SKALDSONG_TTS_ENGINE: "${SKALDSONG_TTS_ENGINE:-kokoro}" SKALDSONG_TTS_BASE_URL: "${SKALDSONG_TTS_BASE_URL:-http://irv-ml1.nh3.internal:8193}" # VibeVoice secondary TTS engine (issue #73, added 2026-05-27). # Same fleet (irv-ml1) as Kokoro, separate port 8194. Encode-once # batch-synth path (long-form bedtime narration); SPA shows a # second "encode with vibevoice" row below the Kokoro playback bar. # Empty/unset → feature stays dormant (routes 404, SPA button hidden). SKALDSONG_TTS_VIBEVOICE_BASE_URL: "${SKALDSONG_TTS_VIBEVOICE_BASE_URL:-}" SKALDSONG_TTS_VIBEVOICE_DEFAULT_VOICE: "${SKALDSONG_TTS_VIBEVOICE_DEFAULT_VOICE:-fable}" # SPA static assets — baked into the image at /app/spa by the # Dockerfile's web-builder stage (COPYs the SvelteKit build output # flat into /app/spa, not into /app/spa/dist). SKALDSONG_HOST_STATIC_ASSETS_PATH: /app/spa # Persistent state paths inside the container. The db/ parent # dir is the bind-mount target (not the file itself) so SQLite # can write its WAL + SHM siblings. Names MUST match what the # app actually reads (per skaldsong's Dockerfile ENV defaults): # SKALDSONG_HOST_SQLITE_PATH + SKALDSONG_HOST_RUNS_ROOT. Earlier # this block used SKALDSONG_DB_PATH / SKALDSONG_RUNS_DIR — those # names are orthogonal to what the app reads, so the app fell # back to Dockerfile defaults pointing at /app/data/... which # is NOT bind-mounted → state wiped on every recreate. SKALDSONG_HOST_SQLITE_PATH: /app/state/db/skaldsong-ui.db SKALDSONG_HOST_RUNS_ROOT: /app/state/runs volumes: # db/ and runs/ are separate bind-mounts so runs/ can later move to # a bigger volume without touching DB state. - ${SKALDSONG_DB_DIR}:/app/state/db - ${SKALDSONG_RUNS_DIR_HOST}:/app/state/runs healthcheck: test: ["CMD-SHELL", "python -c 'import urllib.request,sys; r=urllib.request.urlopen(\"http://127.0.0.1:8000/health\",timeout=3); sys.exit(0 if r.status==200 else 1)' || exit 1"] interval: 30s timeout: 5s retries: 3 start_period: 30s labels: - homepage.group=AI - Gateways & Chat - homepage.name=Skaldsong - homepage.icon=mdi-book-edit-outline - homepage.description=Wizard + reader surface for multi-agent storytelling (Bifrost consumer of Worldtree) - homepage.href=http://10.250.50.70:${SKALDSONG_PORT} networks: default: name: skaldsong_default