# 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.