From 6d6802bac10be9074a6773eb10b621a03f5d36dd Mon Sep 17 00:00:00 2001 From: Aryan Singh Date: Tue, 27 Oct 2026 12:33:32 -0500 Subject: [PATCH] Restore source files from Google Drive backup --- BUILD_PROMPT.md | 203 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 203 insertions(+) create mode 100644 BUILD_PROMPT.md diff --git a/BUILD_PROMPT.md b/BUILD_PROMPT.md new file mode 100644 index 0000000..6ac4c3d --- /dev/null +++ b/BUILD_PROMPT.md @@ -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 `` with the project name before you start. + +--- + +Build ``: 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 +./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.