# Prebuilt image mode: docker compose -f docker-compose.image.yml up -d services: muselab: image: ${MUSELAB_IMAGE:-ghcr.io/hesorchen/muselab:latest} container_name: muselab restart: unless-stopped ports: # Bind to loopback by default — Docker's short syntax otherwise exposes # the port on 0.0.0.0 (all interfaces), which on a public VPS leaves the # service reachable from the internet with only the token as a barrier. # To open it up (e.g. for phone access on the LAN), set MUSELAB_BIND=0.0.0.0 # in .env AND put a reverse proxy / firewall in front. See docs/quickstart. - "${MUSELAB_BIND:-127.0.0.1}:${MUSELAB_PORT:-8765}:8765" env_file: - .env environment: # Force the Agent SDK to use OAuth (subscription), not console API key. ANTHROPIC_API_KEY: "" ANTHROPIC_AUTH_TOKEN: "" # Inside the container the default workspace is always /data; the host # path uses the legacy-compatible ARCHIVE_DIR variable below. MUSELAB_ROOT: /data # Never inherit a native-install host path from .env. Container session # state is persisted by the /app/sessions bind mount below. MUSELAB_SESSIONS_DIR: /app/sessions # Keep mutable settings and third-party canonical transcripts in the # same durable volume. File values override bootstrap env_file values. MUSELAB_CONFIG_DIR: /app/sessions/config MUSELAB_ENV_PATH: /app/sessions/config/.env MUSELAB_ENV_OVERRIDE: "1" XDG_STATE_HOME: /app/sessions/state volumes: # The default workspace exposed for browsing / editing. # Set the legacy-compatible ARCHIVE_DIR in .env (defaults to ./data). - ${ARCHIVE_DIR:-./data}:/data # Reuse the host's claude CLI OAuth credentials. # Default: ~/.claude. Override CLAUDE_HOME in .env for non-standard locations. # # SECURITY NOTE — this mount is read-WRITE on purpose, and that has a real # blast radius: ~/.claude/.credentials.json holds your Pro/Max OAuth # tokens, and the container (which runs an LLM agent that can execute # tools) can read them. A read-only (`:ro`) mount is NOT a drop-in fix — # the CLI needs write access to (a) refresh the OAuth token when it # expires and (b) write session history under ~/.claude/projects/. Making # the whole dir `:ro` breaks token refresh and session persistence. # # To reduce exposure on a shared/hostile host, mount ONLY the subpaths the # CLI actually needs instead of the whole home dir, e.g.: # - ${CLAUDE_HOME:-${HOME}/.claude}/.credentials.json:/home/muse/.claude/.credentials.json # - ${CLAUDE_HOME:-${HOME}/.claude}/projects:/home/muse/.claude/projects # That keeps the rest of ~/.claude (settings, other secrets) off the # container. See SECURITY.md. - ${CLAUDE_HOME:-${HOME}/.claude}:/home/muse/.claude # Persist sessions, UI configuration and third-party transcripts. - ./sessions:/app/sessions # Resource ceilings mirror the systemd unit (see # ~/.config/systemd/user/muselab.service): both deployment paths get the # same "long-running but bounded" envelope. SDK subprocess + token # buffering + cache usually sits well under 1 GiB; 4 GiB is the hard # OOM-kill ceiling that protects the host from a memory leak. mem_limit: 4g mem_reservation: 1g # Cap process / thread count to prevent fork-bomb-style runaway from a # misbehaving MCP server or skill. pids_limit: 4096 healthcheck: # Use the dedicated /api/health endpoint — cheaper than rendering the # full index.html on every probe (every 30s × 24h = 2880 probes/day). # `curl` ships with the image and avoids the ~150ms Python startup cost. test: - CMD - curl - -fsS - --max-time - "3" - http://127.0.0.1:8765/api/health interval: 30s timeout: 5s retries: 3 start_period: 15s