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