# Production Docker Compose file for Beets Web Manager. # # One container. Beets runs inside Web Manager (ADR 0006): a pinned Beets # reads your Beets config.yaml from /config and opens the library and music # folder it names. There is no separate Beets container, no Beets URL and no # plugin to install. # # Usage, from the folder that holds this file (relative paths below resolve # next to it): # docker compose up -d # # Coming from the two-container setup? Follow docs/INSTALLATION.md # ("Upgrading from two containers"): back up, then stop and REMOVE the old # `beets` service. Two programs must never write one library; Web Manager # refuses library changes while it detects the old service. services: beets-web-manager: image: ghcr.io/iranman/beets-web-manager:latest container_name: beets-web-manager restart: unless-stopped ports: - "${WEBCONTROL_PORT:-8337}:8337" environment: PUID: ${PUID:-1000} PGID: ${PGID:-1000} TZ: ${TZ:-UTC} WEBCONTROL_PORT: 8337 # Container-side paths. They must match the mount targets below, and # MUSIC_ROOT must equal Beets' own `directory:` in config.yaml (see # docs/CONFIGURATION.md). Change them only together. MUSIC_ROOT: /music DOWNLOADS_ROOT: /downloads BEETS_CONFIG: /config/config.yaml # Single canonical root for all of Web Manager's own durable state # (job history, transaction/rollback audit logs, submission drafts). WEB_MANAGER_DATA_DIR: /web-manager-data BEETS_TRANSACTION_DIR: /web-manager-data/transactions # All of the following are optional -- .env.example documents them, # and every one is blank/inert by default. None is required to reach # the browser setup wizard. BEETS_WEB_USERNAME: ${BEETS_WEB_USERNAME:-} BEETS_WEB_PASSWORD: ${BEETS_WEB_PASSWORD:-} BEETS_WEB_AUTH_TOKEN: ${BEETS_WEB_AUTH_TOKEN:-} BEETS_WEB_AUTH_DISABLED: ${BEETS_WEB_AUTH_DISABLED:-0} BEETS_TRUSTED_PROXIES: ${BEETS_TRUSTED_PROXIES:-} OPENAI_API_KEY: ${OPENAI_API_KEY:-} OPENROUTER_API_KEY: ${OPENROUTER_API_KEY:-} AI_API_KEY: ${AI_API_KEY:-} AI_BASE_URL: ${AI_BASE_URL:-} AI_MODEL: ${AI_MODEL:-} ACOUSTID_API_KEY: ${ACOUSTID_API_KEY:-} ACOUSTID_USER_KEY: ${ACOUSTID_USER_KEY:-} ACOUSTID_KEY: ${ACOUSTID_KEY:-} DISCOGS_TOKEN: ${DISCOGS_TOKEN:-} LISTENBRAINZ_TOKEN: ${LISTENBRAINZ_TOKEN:-} PLEX_URL: ${PLEX_URL:-} PLEX_TOKEN: ${PLEX_TOKEN:-} LIDARR_URL: ${LIDARR_URL:-} LIDARR_API_KEY: ${LIDARR_API_KEY:-} SLSKD_URL: ${SLSKD_URL:-} SLSKD_API_KEY: ${SLSKD_API_KEY:-} RECOVER_OWNED_ACCOUNTS: ${RECOVER_OWNED_ACCOUNTS:-} SPOTIFY_CLIENT_ID: ${SPOTIFY_CLIENT_ID:-} SPOTIFY_CLIENT_SECRET: ${SPOTIFY_CLIENT_SECRET:-} DEMO_MODE: ${DEMO_MODE:-0} volumes: # Beets config.yaml and musiclibrary.blb (the folder the old Beets # container mounted at /config). - ${BEETS_CONFIG_PATH:-./beets}:/config # Read-write: Beets moves, tags and imports files from this container. - ${MUSIC_PATH:-${MUSIC_LIBRARY_PATH:-./music}}:/music - ${DOWNLOADS_PATH:-${DOWNLOAD_PATH:-./downloads}}:/downloads - ${WEB_MANAGER_DATA_PATH:-${BEETS_WEB_MANAGER_DATA_PATH:-./web-manager}}:/web-manager-data security_opt: - no-new-privileges:true # Drop everything, then add back only what the entrypoint's brief root # phase needs: CHOWN to fix bind-mount ownership, SETUID/SETGID for gosu # to drop to PUID:PGID. The app itself runs unprivileged. cap_drop: - ALL cap_add: - CHOWN - SETUID - SETGID read_only: true tmpfs: - /tmp:rw,noexec,nosuid,nodev,size=512m - /run:rw,nosuid,nodev,size=64m # Unhealthy while Beets' self-test fails (beets.ok false): library # changes are refused then (docs/TROUBLESHOOTING.md lists every code). healthcheck: test: - CMD - python - -c - "import json, sys, urllib.request; d = json.load(urllib.request.urlopen('http://127.0.0.1:8337/api/health', timeout=5)); sys.exit(0 if d.get('beets', {}).get('ok') else 1)" interval: 10s timeout: 5s retries: 5 start_period: 30s