Update installs were failing at the system scanner step because every
release APK was signed with a different signature: build.gradle.kts
fell back to signingConfigs.getByName("debug"), and the debug
keystore is generated per-machine — so every CI runner had a fresh
random key. Android refuses to upgrade an installed app when the
signature doesn't match, hence the "App not installed" failure
after the scan.
Two-part fix:
1. build.gradle.kts now defines a real "release" signing config when
ANDROID_KEYSTORE_PATH is exported (with the matching password +
alias env vars). Without those, falls through to the debug
keystore so local `flutter run --release` still works.
2. flutter.yml decodes ANDROID_KEYSTORE_B64 (CI secret, base64 of the
real release keystore) into a tmp path before the release-APK
build step, then exports ANDROID_KEYSTORE_PATH + the three
password/alias secrets as env vars. CI now hard-errors if the
keystore secret is missing on a tagged build — we don't want
another silent debug-keystore release.
OPERATOR ACTION REQUIRED before the next tag:
1. Generate a release keystore (one-time):
keytool -genkey -v -keystore minstrel-release.keystore \
-alias minstrel -keyalg RSA -keysize 2048 -validity 10000
2. Base64 it:
base64 -w0 minstrel-release.keystore > keystore.b64
3. In Forgejo, add four repo secrets:
ANDROID_KEYSTORE_B64 = contents of keystore.b64
ANDROID_KEY_ALIAS = minstrel
ANDROID_STORE_PASSWORD = the password you set
ANDROID_KEY_PASSWORD = same password (or different alias pwd)
4. Keep minstrel-release.keystore offline and backed up — losing it
means you can never sign an upgrade for any existing install.
5. Existing installs from prior debug-keyed releases must be
uninstalled before installing the first key-signed release. This
is a one-time transition; future tagged builds will upgrade
cleanly.
Minstrel
A self-hosted music server that thinks for you. Smart shuffle, contextual likes, ListenBrainz-aware radio, and Lidarr automation — server-side, so every client (web, mobile, Subsonic third-party) gets the same intelligence.
State and intelligence belong on the server, not the client.
Highlights
- OpenSubsonic-compatible. Existing Subsonic clients (DSub, Symfonium, play:Sub, etc.) connect with no special configuration.
- Server-side smart shuffle. Track-similarity vectors, dual-like model (general + contextual), and session memory keep mixes coherent across devices.
- ListenBrainz radio. Session-aware "more like this" pulls from ListenBrainz similarity data, not a static genre tag.
- Lidarr integration. Triggered scans, request-driven album imports, and a quarantine flow when something doesn't fit.
- Built-in web SPA. Full-feature library, search, queue, playlists, and admin — no separate frontend container to deploy.
- Flutter mobile client in flight. Tracking issue #356.
Quickstart
# compose.yaml
services:
minstrel:
image: git.fabledsword.com/bvandeusen/minstrel:latest
ports: ['4533:4533']
volumes:
- ./music:/music:ro
- minstrel-data:/data
environment:
MINSTREL_DATABASE_URL: postgres://minstrel:minstrel@db:5432/minstrel?sslmode=disable
MINSTREL_LIBRARY_SCAN_PATHS: /music
depends_on: [db]
db:
image: postgres:17
environment:
POSTGRES_USER: minstrel
POSTGRES_PASSWORD: minstrel
POSTGRES_DB: minstrel
volumes: [pgdata:/var/lib/postgresql/data]
volumes:
minstrel-data:
pgdata:
docker compose up -d
After the stack is up, visit http://localhost:4533/register and create your admin account. The first user to register on a fresh instance is automatically marked as the administrator; subsequent users can register through the same form (or via invite tokens generated from the admin Users panel, depending on how you configure registration).
For the full configuration surface, see config.example.yaml.
Configuration
Most operators only need the env vars in the quickstart above. A few extras worth knowing:
MINSTREL_BRANDING_APP_NAME— rename the instance ("Family Jukebox", "Office Music"). Surfaces in the header, browser tab, and OG share previews.MINSTREL_STORAGE_DATA_DIR— defaults to./data. Holds playlist cover collages and other generated artefacts.MINSTREL_LIBRARY_SCAN_PATHS— colon-separated list of music library roots to scan. Supports multiple roots (/music:/podcasts).
ListenBrainz integration (per-user scrobble + similarity tokens) and Lidarr integration (URL + API key) are configured through the admin Settings UI rather than env vars or yaml — per Minstrel's "config in UI" rule, integration settings live where operators can edit them without restarting.
Most operational keys have a MINSTREL_<SECTION>_<FIELD> env override. Recommendation and events tuning are yaml-only. See config.example.yaml for the authoritative surface.
Updating
:main— rolling, follows the dev branch's tested tip. Recommended only for the operator who's running an upstream-watching deployment.:v1.0.x— pinned releases. Recommended default. Database migrations run automatically at startup; rollbacks require restoring a Postgres dump.
Specs
Authoritative scope lives under docs/:
- Server spec — current implementation focus.
- Client spec — Flutter companion app.
Development
Two concurrent dev processes:
- Backend:
docker compose up— Postgres + Minstrel on:4533. - Frontend:
cd web && npm install && npm run dev— Vite dev server on:5173with HMR. The Vite server proxies/api/*and/rest/*to:4533so session cookies work.
Production build
docker build -t minstrel . runs the SvelteKit build inside a node stage, copies the output into the golang stage, and //go:embeds it into the final binary. The container serves the SPA from / alongside the API surfaces; no separate static-file server is required.
Branches
- Day-to-day work happens on
dev(or feature branches merged intodev). mainis protected — changes land via PR fromdev.- Releases are cut by tagging
v*offmain; the release workflow builds and pushes the container image to the Forgejo registry.
Task and milestone tracking: Fable (Minstrel project, id 12).
License
See LICENSE.