- docs/hosting.md: LAN vs internet; binding 4533 to 127.0.0.1 behind an HTTPS proxy (Caddy example, no buffering, long read timeouts for SSE and streams); the Client IP detection hop count (default 1, so 0 with no proxy or clients can forge X-Forwarded-For); the public address that password-reset links need; finding the setup token. - docs/security.md: sessions, API keys, rate limits, headers and CSP; why CSRF rests on SameSite=Strict plus JSON-only cookie writes; the Subsonic password column, including the known issue that admin reset-password writes the login password there (#5026); why Android allows plain HTTP; the CI publish gate. - README: keeps the LAN-first port mapping with a pointer for internet hosts, scopes "plain http:// is fine" to trusted networks, explains the setup token in first-run step 1, and links both docs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
93 lines
3.4 KiB
Markdown
93 lines
3.4 KiB
Markdown
# Hosting Minstrel
|
|
|
|
Minstrel serves plain HTTP on port 4533 and never terminates TLS itself. On a
|
|
home network or a VPN you trust, that is all you need. Once the server is
|
|
reachable from the internet, put it behind a reverse proxy that speaks HTTPS,
|
|
and tell Minstrel about that proxy. This page covers both.
|
|
|
|
## On a LAN or VPN
|
|
|
|
Publish the port to the network and use `http://<host>:4533`:
|
|
|
|
```yaml
|
|
ports: ['4533:4533']
|
|
```
|
|
|
|
If nothing sits in front of Minstrel, set **Admin → Integrations → Client IP
|
|
detection** to 0. It defaults to 1, which assumes one proxy (see below for
|
|
why that matters).
|
|
|
|
## On the internet
|
|
|
|
Three things, all required:
|
|
|
|
1. **Don't publish 4533 to the world.** Bind it to the loopback interface so
|
|
only the proxy on the same host can reach it:
|
|
|
|
```yaml
|
|
ports: ['127.0.0.1:4533:4533']
|
|
```
|
|
|
|
If the proxy runs in another container on the same Docker network, drop
|
|
`ports:` entirely and point the proxy at `minstrel:4533`.
|
|
|
|
2. **Terminate HTTPS at a reverse proxy.** Any proxy works. With Caddy, which
|
|
gets and renews certificates by itself:
|
|
|
|
```
|
|
music.example.com {
|
|
reverse_proxy 127.0.0.1:4533
|
|
}
|
|
```
|
|
|
|
Two settings matter for every proxy:
|
|
- **Don't buffer responses.** Live updates use Server-Sent Events and audio
|
|
is streamed; a buffering proxy holds both back. Caddy streams by default.
|
|
For nginx, set `proxy_buffering off;`.
|
|
- **Allow long-lived responses.** An event stream stays open for as long as
|
|
the app is. Raise the proxy's read timeout well past a minute (nginx:
|
|
`proxy_read_timeout 1h;`).
|
|
|
|
3. **Tell Minstrel where it lives**, in **Admin → Integrations**:
|
|
- **Client IP detection:** the number of proxies in front of Minstrel. One
|
|
proxy (Caddy, Traefik, nginx) is 1; Cloudflare in front of Traefik is 2.
|
|
Minstrel uses this to find the real client address, for login rate
|
|
limits and the sessions list, and to tell whether a request arrived over
|
|
HTTPS.
|
|
- **Public address:** the URL people use, e.g. `https://music.example.com`.
|
|
Password-reset emails link here. **Until it is set, no reset email is
|
|
sent**, so a forged `Host` header can never put a reset token on a link
|
|
to someone else's server.
|
|
|
|
### Why the proxy count matters
|
|
|
|
Minstrel only believes `X-Forwarded-For` and `X-Forwarded-Proto` from the
|
|
number of proxies you configure, counted from the right. Set it too high, or
|
|
leave it at the default of 1 with no proxy in front, and a client can write
|
|
those headers itself: it could claim any address to slip past the per-address
|
|
login limit, or claim HTTPS. With no proxy, set it to 0.
|
|
|
|
When the proxy reports HTTPS, Minstrel marks the session cookie `Secure` and
|
|
sends `Strict-Transport-Security`. Over plain HTTP it does neither, and it
|
|
never redirects to HTTPS, so LAN use keeps working unchanged.
|
|
|
|
## First account
|
|
|
|
A fresh server with no accounts prints a **setup token** in its log:
|
|
|
|
```
|
|
docker compose logs minstrel | grep setup_token
|
|
```
|
|
|
|
Creating the first account (which becomes the admin) requires that token, so
|
|
whoever reaches a newly exposed server first can't claim it. The token
|
|
changes on every restart until an account exists.
|
|
|
|
## Android app
|
|
|
|
The app connects over whatever URL you give it, HTTP or HTTPS. Once the
|
|
server has a public HTTPS address, give the app that address so the session
|
|
cookie never crosses the internet unencrypted. See
|
|
[security notes](./security.md#android-allows-plain-http) for why plain HTTP
|
|
is still allowed.
|