Files
Lyra/docker-compose.yml
T
Jonathan 190e0ef4b6 feat(web): minimal shared-password auth gate
The web app had no auth — anyone reaching the host could browse the library,
trigger downloads, and read/overwrite the stored credentials in Settings. Adds
an opt-in single-shared-password gate:

- Next.js middleware redirects unauthenticated page requests to /login and
  returns 401 for /api/*; static assets are excluded via matcher.
- Auth is enabled ONLY when LYRA_PASSWORD is set (unset ⇒ app stays open, so
  existing deployments are unchanged). Documented in README "Security".
- The session cookie is httpOnly and carries HMAC-SHA256(password) keyed by
  LYRA_SECRET_KEY — unforgeable, and the plaintext password never touches the
  client. Edge-safe Web Crypto so one lib/auth.ts serves middleware + route.
- Cookie is NOT Secure: Lyra serves plain HTTP on the LAN; a Secure cookie would
  loop. Works behind an HTTPS proxy too.
- /login page + form, a Sign out button (shown only when auth is enabled), and
  chrome (nav + worker-status poll) suppressed on /login.
- docker-compose passes LYRA_PASSWORD to web; .env.example documents it.

Verified live end-to-end (built + next start): no-cookie page→307 /login,
API→401, wrong password→401, correct→200+cookie, authed page/API→200, forged
cookie→401, and the auth-disabled (no LYRA_PASSWORD) path stays fully open.
web 133 tests green, tsc clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 10:12:51 +02:00

102 lines
3.8 KiB
YAML

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:-}
ports:
# host:container — host 8770 avoids the Quill app on 3000 (and Lidarr on 8686)
- "8770:3000"
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
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: