services: db: image: postgres:17 restart: unless-stopped environment: POSTGRES_USER: ${POSTGRES_USER} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} POSTGRES_DB: ${POSTGRES_DB} volumes: - postgres-data:/var/lib/postgresql/data ports: - "5432:5432" healthcheck: test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"] interval: 5s timeout: 5s retries: 5 web: build: ./web restart: unless-stopped environment: DATABASE_URL: ${DATABASE_URL} LYRA_SECRET_KEY: ${LYRA_SECRET_KEY} # Optional shared-password gate for the whole UI/API. Unset ⇒ app is open. See README. LYRA_PASSWORD: ${LYRA_PASSWORD:-} # Next's standalone server binds to HOSTNAME; default is the container hostname (its own # IP only), so loopback healthchecks can't reach it. Bind all interfaces instead. HOSTNAME: 0.0.0.0 ports: # host:container — host 8770 avoids the Quill app on 3000 (and Lidarr on 8686) - "8770:3000" healthcheck: # unauthenticated liveness endpoint; node 22 has global fetch (no curl/wget in the image) test: ["CMD-SHELL", "node -e \"fetch('http://127.0.0.1:3000/api/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))\""] interval: 30s timeout: 5s retries: 3 start_period: 20s depends_on: db: condition: service_healthy worker: build: ./worker restart: unless-stopped environment: DATABASE_URL: ${DATABASE_URL} LYRA_SECRET_KEY: ${LYRA_SECRET_KEY} STAGING_ROOT: /staging SLSKD_DOWNLOADS_ROOT: /slskd-downloads # Qobuz's CDN also resolves to IPv6, but the container has no IPv6 route # ("Network is unreachable"); disable IPv6 so downloads use IPv4 only. sysctls: - net.ipv6.conf.all.disable_ipv6=1 volumes: - ${MUSIC_DIR:-./music}:/music # Per-job download staging on its own volume. Defaults to MUSIC_DIR/.staging (prior # behavior); set STAGING_DIR to a fast local path when MUSIC_DIR is a network share. - ${STAGING_DIR:-${MUSIC_DIR:-./music}/.staging}:/staging # slskd (an EXTERNAL daemon, not in this compose stack) writes finished Soulseek # downloads into its own directory; the worker moves them out of here into staging. # Point SLSKD_DOWNLOADS_DIR at slskd's downloads dir on the host (must be the SAME # path slskd writes to — set it once Lyra runs on the slskd host). Defaults to an # empty local dir so the mount is always valid; Soulseek delivery only works once # this points at the real slskd downloads directory. - ${SLSKD_DOWNLOADS_DIR:-./slskd-downloads}:/slskd-downloads healthcheck: # the worker touches /tmp/worker.alive every ~15s from a background thread (survives long # downloads); unhealthy if it hasn't been touched in 90s (process wedged/exited). test: ["CMD-SHELL", "test $(( $(date +%s) - $(stat -c %Y /tmp/worker.alive 2>/dev/null || echo 0) )) -lt 90"] interval: 30s timeout: 5s retries: 3 start_period: 20s depends_on: db: condition: service_healthy # Scheduled Postgres backups. ALL durable state (library, monitored/wanted lists, # discovery data, config, AND the encrypted credentials) lives in the single # postgres-data volume — one volume loss = total loss. This sidecar pg_dumps on an # interval (immediately on start, then every BACKUP_INTERVAL_SECONDS), keeping the # last BACKUP_KEEP dumps in ${BACKUP_DIR:-./backups}. Restore steps: see README. # For off-box safety, point BACKUP_DIR at a synced/NAS path or rsync ./backups away. db-backup: image: postgres:17 restart: unless-stopped environment: PGHOST: db PGUSER: ${POSTGRES_USER} PGPASSWORD: ${POSTGRES_PASSWORD} PGDATABASE: ${POSTGRES_DB} BACKUP_INTERVAL_SECONDS: ${BACKUP_INTERVAL_SECONDS:-86400} BACKUP_KEEP: ${BACKUP_KEEP:-7} volumes: - ${BACKUP_DIR:-./backups}:/backups depends_on: db: condition: service_healthy entrypoint: ["/bin/sh", "-c"] command: - | echo "db-backup: starting (interval=$${BACKUP_INTERVAL_SECONDS}s keep=$${BACKUP_KEEP})" while true; do ts=$$(date +%Y%m%d-%H%M%S) out="/backups/lyra-$${ts}.dump" if pg_dump -Fc -f "$${out}.tmp" 2>/tmp/err; then mv "$${out}.tmp" "$${out}" echo "db-backup: wrote $${out} ($$(du -h "$${out}" | cut -f1))" else echo "db-backup: pg_dump FAILED: $$(cat /tmp/err)"; rm -f "$${out}.tmp" fi ls -1t /backups/lyra-*.dump 2>/dev/null | tail -n +$$(( $${BACKUP_KEEP} + 1 )) | xargs -r rm -f sleep "$${BACKUP_INTERVAL_SECONDS}" done volumes: postgres-data: