Compare commits
2
Commits
+203
@@ -0,0 +1,203 @@
|
||||
# Build prompt — a self-hostable music request stack
|
||||
|
||||
Paste this into Claude Code in a **fresh empty repo**. It assumes no prior
|
||||
context. Replace `<NAME>` with the project name before you start.
|
||||
|
||||
---
|
||||
|
||||
Build `<NAME>`: a one-command, self-hostable stack that turns "I want this
|
||||
song" into a correctly-tagged file in a personal music library, reachable from
|
||||
any Subsonic client.
|
||||
|
||||
You are creating a public open-source project that strangers will run on their
|
||||
own hardware. Optimise for *someone else's first hour*, not for my convenience.
|
||||
|
||||
## What it is
|
||||
|
||||
Four containers, orchestrated:
|
||||
|
||||
| service | role | image |
|
||||
|---|---|---|
|
||||
| `gluetun` | VPN killswitch that slskd lives behind | `qmcgaw/gluetun` (pin the tag) |
|
||||
| `slskd` | the Soulseek client — the only thing that speaks the protocol | `slskd/slskd` |
|
||||
| `muse` | web front door: search, request, review, admin | `ghcr.io/geneticglitch1/muse` |
|
||||
| `lidarr` | optional: album-level wishlist, retag, import | `lscr.io/linuxserver/lidarr` |
|
||||
|
||||
`muse` is an existing project (github.com/geneticglitch1/muse) — pull its image,
|
||||
do not rebuild or vendor it. Read its README for its environment contract.
|
||||
|
||||
Optionally include `soularr` (`mrusse08/soularr`) to bridge Lidarr's wanted
|
||||
list to slskd, and `navidrome` (`deluan/navidrome`) so the stack is complete
|
||||
end to end for someone with no existing library server.
|
||||
|
||||
## Non-negotiable constraints
|
||||
|
||||
These are not preferences. Each one is a bug I have already paid for.
|
||||
|
||||
**1. Sharing is ON by default, and this is a compliance requirement.**
|
||||
|
||||
Soulseek's rules tolerate alternative clients that "implement the full range of
|
||||
features … including chat, search, wishlist, download, **upload**, and respect /
|
||||
recognition of privileges". A download-only deployment is the thing that rule
|
||||
exists to prevent. So:
|
||||
|
||||
- `slskd` ships with the music library mounted read-only and shared.
|
||||
- The README states plainly, near the top, that the stack shares by default and
|
||||
that this is both the network's rule and the reason the network works.
|
||||
- Disabling sharing must require deliberately editing a value that is commented
|
||||
as antisocial and rule-breaking. Never make it a convenient flag.
|
||||
- Do not add any feature that hides, fakes, or minimises upload participation.
|
||||
|
||||
**2. The VPN must fail closed.**
|
||||
|
||||
If the tunnel drops, slskd must lose network access, not fall back to the naked
|
||||
connection. `network_mode: "service:gluetun"` plus a healthcheck, plus
|
||||
`depends_on: {condition: service_healthy, restart: true}`. Verify this is real
|
||||
by documenting a test that stops gluetun and shows slskd cannot reach the
|
||||
internet.
|
||||
|
||||
**3. AirVPN (and similar) use STATIC reserved ports — gluetun cannot fetch them.**
|
||||
|
||||
gluetun's dynamic port forwarding does not work for AirVPN. Do not set
|
||||
`VPN_PORT_FORWARDING` or slskd's `SLSKD_VPN_PORT_FORWARDING` for those
|
||||
providers — slskd will wait forever for a port that never arrives. Instead the
|
||||
user reserves a port in the provider's control panel and sets it in `.env`, and
|
||||
the compose passes it to **both** `FIREWALL_VPN_INPUT_PORTS` (so inbound peers
|
||||
are admitted on `tun0`) and `SLSKD_SLSK_LISTEN_PORT`.
|
||||
|
||||
Do **not** publish the Soulseek listen port with a docker `ports:` entry. Docker
|
||||
DNAT does nothing for traffic arriving over the tunnel. Say this in a comment,
|
||||
because everyone tries it.
|
||||
|
||||
This distinction — static-reservation providers vs. dynamic-forwarding
|
||||
providers — needs its own docs section with a table, because getting it wrong
|
||||
presents as "downloads are just slow and flaky", never as an error.
|
||||
|
||||
**4. One `/data` mount, never two.**
|
||||
|
||||
`link()` between two different *mounts* returns `EXDEV` even when both are on
|
||||
the same filesystem. Split downloads and library into separate mounts and every
|
||||
import silently degrades to a full copy, storing the library twice. Mount a
|
||||
single `/data` and use subdirectories. Ship a preflight script that creates a
|
||||
hardlink across the two subdirectories and fails loudly if it can't.
|
||||
|
||||
**5. Bind-mount targets must pre-exist, owned by the runtime UID.**
|
||||
|
||||
Docker creates missing bind targets as `root:root`, which breaks every
|
||||
container running as a non-root user. The setup script creates them with the
|
||||
right ownership before first start.
|
||||
|
||||
## Repository shape
|
||||
|
||||
```
|
||||
docker-compose.yml profiles: vpn (default), no-vpn, lidarr, navidrome
|
||||
.env.example every variable, commented, no real values
|
||||
setup.sh interactive first-run: dirs, perms, secrets, preflight
|
||||
docs/
|
||||
README.md landing: what it is, 5-minute quickstart
|
||||
vpn.md providers, static vs dynamic ports, verification
|
||||
exposing.md reverse proxy, TLS, auth — the safe path
|
||||
sharing.md why sharing is on, what the rules say, etiquette
|
||||
troubleshooting.md symptom -> cause table
|
||||
architecture.md what each container does and why it's separate
|
||||
.github/workflows/ lint compose, shellcheck, link-check the docs
|
||||
LICENSE AGPL-3.0 or MIT — ask me which before writing it
|
||||
```
|
||||
|
||||
## The quickstart must actually be five minutes
|
||||
|
||||
```bash
|
||||
git clone … && cd <NAME>
|
||||
./setup.sh # asks for VPN creds, reserved port, Soulseek login
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
`setup.sh` is the difference between a project people use and a project people
|
||||
star. It should:
|
||||
|
||||
- detect whether `docker compose` exists and is v2
|
||||
- ask which VPN provider, and branch the port question on static vs dynamic
|
||||
- generate random values for every secret that can be random (`slskd` API key,
|
||||
admin password) rather than asking
|
||||
- create directories with correct ownership
|
||||
- run the hardlink preflight and **refuse to continue** if it fails, explaining
|
||||
why in terms of the consequence ("your library will be stored twice")
|
||||
- write `.env` and print exactly what to do next
|
||||
- be idempotent — safe to re-run
|
||||
|
||||
## Exposing it safely
|
||||
|
||||
This is the section that decides whether the project harms its users, so it
|
||||
gets real care. Cover, in this order of preference:
|
||||
|
||||
1. **Don't.** Tailscale / WireGuard / LAN-only. Recommended default, stated as
|
||||
such. Most people asking do not actually need public access.
|
||||
2. **Reverse proxy with OAuth in front.** Caddy and nginx-proxy-manager configs.
|
||||
muse has GitHub/Google OAuth built in and fails closed without it — show how
|
||||
to configure that, and note the callback URL rule.
|
||||
3. **Never expose slskd or Lidarr directly.** Their auth is not built for the
|
||||
open internet. Only muse, and only behind OAuth.
|
||||
|
||||
Include a short "if you expose this, understand" list: the VPN protects your
|
||||
Soulseek traffic, not your web UI; your reverse proxy sees your real IP; the
|
||||
admin panel can delete files.
|
||||
|
||||
## Compliance and legality — one honest section, no hedging
|
||||
|
||||
A `docs/legal.md`, linked from the README, stating plainly:
|
||||
|
||||
- Soulseek's rules tolerate full-featured alternative clients; slskd is one, and
|
||||
this stack does not modify its protocol behaviour.
|
||||
- Uploading is part of that; this stack shares by default and you should not
|
||||
turn it off.
|
||||
- Whether any given file is legal to download is on the operator, exactly as
|
||||
with any download client. Quote Soulseek's own line: users should only share
|
||||
and download files they are legally allowed to.
|
||||
- This project is not affiliated with or endorsed by Soulseek, and does not
|
||||
bundle or redistribute the official Soulseek client.
|
||||
|
||||
Say it once, clearly, and do not repeat it apologetically elsewhere.
|
||||
|
||||
## Engineering standards
|
||||
|
||||
- **Multi-arch.** amd64 and arm64. A large share of self-hosters are on a Pi or
|
||||
an Apple-silicon Mac; an amd64-only compose is a wall of exec-format errors.
|
||||
- **Pin every image tag.** `:latest` on a VPN container means an unattended pull
|
||||
can drop the tunnel. Pin, and document how to update deliberately.
|
||||
- **Healthchecks on everything** that something else depends on.
|
||||
- **Resource limits** and log rotation (`max-size`, `max-file`) — an unbounded
|
||||
json-file log has filled a homelab disk before.
|
||||
- **No secrets in the repo, ever.** `.env` is gitignored; `.env.example` holds
|
||||
placeholders only. Add a CI check that greps for anything key-shaped.
|
||||
- **Comment the non-obvious, not the obvious.** Every constraint above earns a
|
||||
comment explaining the *consequence* of getting it wrong, not a restatement of
|
||||
the line of YAML.
|
||||
|
||||
## Verification — do this, don't assume it
|
||||
|
||||
Before telling me it works:
|
||||
|
||||
1. `docker compose config` renders with `.env.example` values filled in.
|
||||
2. Both VPN profiles come up; `docker exec gluetun wget -qO- ifconfig.me` shows
|
||||
a non-residential IP, and the same command in a no-VPN profile shows yours.
|
||||
3. Kill gluetun; confirm slskd has no internet. This is the killswitch test.
|
||||
4. The hardlink preflight passes, and a file imported into the library shows
|
||||
link count 2.
|
||||
5. slskd's UI reports connected, logged in, and a non-zero shared file count.
|
||||
6. muse loads, search returns results, a request reaches slskd.
|
||||
7. `shellcheck setup.sh` is clean; `setup.sh` is idempotent.
|
||||
|
||||
Report what you actually ran and what it printed. If something can't be tested
|
||||
in your environment, say so explicitly rather than implying it passed.
|
||||
|
||||
## Work order
|
||||
|
||||
Ask me about the licence and the name first if I haven't said. Then:
|
||||
|
||||
1. `docker-compose.yml` + `.env.example` + `setup.sh` — the working core.
|
||||
2. Verify it, per the list above.
|
||||
3. Docs, written against the thing that now exists rather than the plan.
|
||||
4. CI.
|
||||
|
||||
Show me the compose file and get agreement before writing the docs. Don't
|
||||
generate all of it and ask me to review a hundred files at once.
|
||||
Reference in new issue
Block a user