Files
Lyra/docs/DEPLOYMENT.md
Jonathan ccaaa41fec docs: slim README for self-hosting, move advanced topics to docs/DEPLOYMENT.md
Restructure the docs ahead of sharing the project for others to self-host. The
README is now a focused get-it-running path (overview, architecture, prereqs,
quick start, operational notes, security, backup/restore). Moved the fuller
feature tour, the maintainer pre-built-image release flow, and the Gluetun VPN
guide into a new docs/DEPLOYMENT.md, cross-linked both ways. The pre-built-image
section is now labelled a maintainer process with a LYRA_REGISTRY override so a
self-hoster can point at their own registry (or just build from source).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 02:57:45 +02:00

8.2 KiB

Lyra — deployment & advanced guide

Topics beyond the README quick-start: the full feature tour, publishing pre-built images to a registry, and running the whole stack behind a VPN. For a basic self-hosted setup you only need the README — start there.


Features in depth

Lyra was built in three slices, all complete:

  1. Acquisition engine. Resolve a request against MusicBrainz, match/rank across Qobuz/Soulseek/YouTube, download the best source into an isolated staging dir, verify completeness, tag (mutagen), and atomically promote the clean album into Artist/Album (Year)/## Title.ext.
  2. Library manager. Watched artists (live MB search + follow), per-release monitor toggles + auto-monitor-future, a wanted list with retry + quality-upgrade window, and a worker "monitor" that discovers new releases and auto-grabs through the acquisition pipeline. Plus a library scan that ingests existing on-disk albums as have + monitored + followed, and a per-artist studio-only / show-all release-type filter.
  3. Discovery. Recommendation-driven finding of new music via ListenBrainz similar-artists and Last.fm artist.getSimilar (both MBID-native), behind a pluggable SimilaritySource interface — Last.fm registers as a second source automatically once its API key is set, and cross-source scores are summed per candidate. A background sweep (and a "Discover now" button) aggregates artists similar to the ones you follow into a manual review feed of suggested artists and albums at /discover; Follow and Want reuse the slice-2 follow / wanted flows, Dismiss is permanent. Plus a web-side seed-search box. Every suggestion and seed-search result opens a read-only artist preview (/discover/artist/[mbid]) — discography, per-album tracklists, and "in library" / "monitored" badges — where you can Follow or Want before committing. No Spotify (its recommendation APIs are dead for new apps).

Alongside the three slices:

  • Last.fm browse (/lastfm) — your Last.fm top artists and albums (by period), with a "hide items already in my library" toggle and inline Follow / Want buttons on each row; each artist also opens a modal of your most-played songs and albums.
  • Library (/library) — a cover-art grid of everything on disk, filterable, each album opening a tracklist modal.
  • The Floor (home) — a live queue of in-flight acquisitions with three-phase progress (Searching → Downloading → Finishing), a live download percentage, a per-job "pressing ticket" (source · format · tracks · elapsed), and a Retry button on anything that needs attention.

The whole UI runs a hand-rolled "Pressing Plant" design system (warm-paper theme, self-hosted Fraunces serif for prose vs. system mono for machine data, light/dark aware).

Deferred hardening and the full roadmap live in the implementation-plan "Notes" sections under docs/superpowers/plans/; the full design specs are under docs/superpowers/specs/.


Publishing pre-built images

Maintainer release process — this is how the project maintainer publishes images to a private registry. If you're self-hosting your own copy, ignore this and just build from source with docker compose up -d --build (see the README). You'd only need this if you run your own registry and want servers to pull rather than build.

The web and worker images can be published to a container registry and consumed by a compose file that pulls instead of builds. The maintainer publishes to a Gitea container registry consumed by a compose file in the vm-download ops repo (docker/lyra/).

Publish a new release from this repo (amd64):

docker login <your-registry>       # once — registry user + a token with package:write
./scripts/build-push.sh            # builds + pushes lyra-web / lyra-worker (:latest and :<sha>)

scripts/build-push.sh defaults the registry namespace to git.jger.nl/jonathan; override it with LYRA_REGISTRY=<your-registry>/<namespace>.

Then on the server:

cd /opt/git/vm-download && git pull
cd docker/lyra && docker compose pull && docker compose up -d

db (Postgres) and db-backup use the stock postgres:17 image, so only the two custom images are built here.


Running behind a VPN (Gluetun)

Lyra runs fine with its traffic routed through a Gluetun VPN container — useful for hiding the download traffic (Soulseek / Qobuz / YouTube). Nothing in Lyra is VPN-incompatible; the only thing that changes is container networking, because a service using network_mode: "service:gluetun" gives up its own network stack and shares Gluetun's. Two consequences:

  • Containers that share Gluetun's namespace reach each other over localhost, not by Docker service name — so DATABASE_URL changes from @db:5432 to @localhost:5432.
  • Published ports move to the gluetun container (the individual services can no longer publish their own).

The simplest, most consistent setup is to put the whole stack behind Gluetun. Sketch (merge into your existing Gluetun compose; adjust the VPN provider block to yours):

services:
  gluetun:
    image: qmcgaw/gluetun
    cap_add: [NET_ADMIN]
    environment:
      # ... your VPN provider / credentials ...
      FIREWALL_INPUT_PORTS: "3000"                 # let your browser reach the web UI
      FIREWALL_OUTBOUND_SUBNETS: "192.168.0.0/16"  # your LAN — so the worker can reach slskd
    ports:
      - "8770:3000"   # Lyra web  (published on gluetun, not on `web`)
      # - "5432:5432" # only if you want DB access from the LAN

  web:
    build: ./web
    network_mode: "service:gluetun"     # no `ports:` / `networks:` here
    environment:
      DATABASE_URL: postgresql://lyra:lyra@localhost:5432/lyra   # `db` -> `localhost`
      LYRA_SECRET_KEY: ${LYRA_SECRET_KEY}
      LYRA_PASSWORD: ${LYRA_PASSWORD:-}
      HOSTNAME: 0.0.0.0
      LYRA_LIBRARY_ROOT: /music
    volumes:
      - ${MUSIC_DIR:-./music}:/music
    depends_on:
      gluetun: { condition: service_healthy }
      db: { condition: service_healthy }

  worker:
    build: ./worker
    network_mode: "service:gluetun"
    environment:
      DATABASE_URL: postgresql://lyra:lyra@localhost:5432/lyra
      LYRA_SECRET_KEY: ${LYRA_SECRET_KEY}
      STAGING_ROOT: /staging
      SLSKD_DOWNLOADS_ROOT: /slskd-downloads
    volumes:
      - ${MUSIC_DIR:-./music}:/music
      - ${STAGING_DIR:-${MUSIC_DIR:-./music}/.staging}:/staging
      - ${SLSKD_DOWNLOADS_DIR:-./slskd-downloads}:/slskd-downloads
    depends_on:
      gluetun: { condition: service_healthy }
      db: { condition: service_healthy }

  db:
    image: postgres:17
    network_mode: "service:gluetun"     # shares the namespace so `localhost:5432` resolves
    environment:
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: ${POSTGRES_DB}
    volumes:
      - postgres-data:/var/lib/postgresql/data

(Give db-backup the same network_mode and PGHOST: localhost.) The HOSTNAME=0.0.0.0 and the built-in healthchecks keep working, since everything is in one namespace.

Caveats worth knowing:

  • Qobuz is geo-sensitive — its catalog/availability varies by country, so pick a VPN exit in a region where your Qobuz account works, or some downloads will fail to match.
  • The worker's net.ipv6.conf.all.disable_ipv6 sysctl can't apply behind Gluetun (a shared netns can't set sysctls) — harmless, because Gluetun blocks IPv6 anyway, which is exactly what that sysctl worked around (Qobuz's CDN resolving to an unroutable IPv6).
  • MusicBrainz rate-limits per IP; a shared VPN exit is marginally more likely to hit the 1 req/s ceiling, but the built-in response cache makes that a non-issue in normal use.
  • Bind mounts (/music, /staging, /slskd-downloads), the Postgres volume, and backups are filesystem, not network — completely unaffected by the VPN.