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>
This commit is contained in:
@@ -0,0 +1,177 @@
|
||||
# Lyra — deployment & advanced guide
|
||||
|
||||
Topics beyond the [README](../README.md) 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](#features-in-depth)
|
||||
- [Publishing pre-built images](#publishing-pre-built-images)
|
||||
- [Running behind a VPN (Gluetun)](#running-behind-a-vpn-gluetun)
|
||||
|
||||
---
|
||||
|
||||
## 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/`](superpowers/plans/); the full design specs are under
|
||||
[`docs/superpowers/specs/`](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`](https://git.jger.nl/Jonathan/vm-download) ops repo (`docker/lyra/`).
|
||||
|
||||
Publish a new release from this repo (amd64):
|
||||
|
||||
```sh
|
||||
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:
|
||||
|
||||
```sh
|
||||
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](https://github.com/qdm12/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):
|
||||
|
||||
```yaml
|
||||
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.
|
||||
Reference in New Issue
Block a user