release / web (push) Failing after 15s
release / govulncheck (push) Successful in 16s
release / go (push) Failing after 41s
release / integration (push) Failing after 3m29s
release / android (push) Successful in 4m7s
release / Attach APK to the Release (tag releases only) (push) Canceled after 0s
release / Build + push container image (push) Canceled after 0s
release / Verify release artifacts (tag releases only) (push) Canceled after 0s
release / Build signed APK (push) Canceled after 5m34s
Admin › Backups shows a verdict first (off, not set up, waiting, backed up, overdue, failing since when), Back up now, Test destination and the latest runs, with the settings beside them. - backup.Service: one run at a time, bounded at 30 minutes; the schedule ticks every 5 minutes, catches up a slot missed while the server was down, and retries a failure after an hour and then after as long as it has been failing. Runs are recorded in backup_runs, pruned after 90 days; a run cut off by a shutdown is marked failed at the next start. - A folder destination writes database/minstrel-<UTC stamp>.tar and refuses a folder that isn't there. An S3 destination stages the file, hashes it, and uploads it under database/ through a throttled reader (4 MiB/s by default, 0 for no limit). - Test destination checks the form as typed and only lists. - The bucket secret is sealed like every stored credential; backup_settings and backup_runs are left out of the backup itself. - The panel polls only while a run is going, capped at the run limit plus five minutes. The music switch named in the task is left to #6774, so no control ships that does nothing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
112 lines
4.4 KiB
Markdown
112 lines
4.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 › Email & network → 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 › Email & network**:
|
||
- **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.
|
||
|
||
## Backups
|
||
|
||
Minstrel can back itself up once a night: accounts, likes, playlists,
|
||
listening history, requests and settings, in one file. It is off until an
|
||
admin switches it on under **Admin › Backups**. Anything Minstrel can fetch
|
||
or work out again (artwork, fingerprints, MusicBrainz data) is left out, and
|
||
so are the keys and passwords Minstrel keeps for other services, which are
|
||
entered again on a restored server. Sign-in passwords are kept.
|
||
|
||
A backup goes to one of:
|
||
- **an S3 bucket**, path-style (MinIO, Garage, or any S3-compatible store).
|
||
The key only needs to add and list objects. Files land under `database/`.
|
||
- **a folder** on the server, under `database/` inside it. Mount a volume
|
||
there; Minstrel refuses to write into a folder that isn't there.
|
||
|
||
Minstrel only adds files, named by date, and never deletes one. Keep as many
|
||
as you like with the destination's own retention. **Test destination** lists
|
||
what is there without writing anything.
|
||
|
||
## 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.
|