Compare commits
1
Commits
-203
@@ -1,203 +0,0 @@
|
|||||||
# 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