- Navidrome password is now passed to curl via stdin (--data-urlencode "p@-") in navidrome-scan.sh and pipeline-status.sh, so it no longer appears in ps/proc. Verified the query sent is identical and a live scan still triggers. - MIN_ARTIST_DIRS (the share-health gate) is now a setting, threaded through to the pipeline env, so a user with a small library can lower it instead of the scan/sync being permanently blocked by the hardcoded 500. - /auth/logout is now POST-only (with a nav form + aria-label), so a drive-by GET can't log the user out; enforced allowed_email already landed separately. - view_log now confirms the run's log_path resolves under the logs dir before serving it (defense in depth). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
alembic
A self-hosted control panel that turns your Spotify playlists into a real, well-tagged music library on your own server, downloaded automatically, kept up to date, and free of duplicate junk.
No terminal required for day-to-day use. Everything, adding playlists, fixing a mislabeled song, cleaning up duplicates, happens through a web page.
Table of contents
- What alembic does
- What you need before you start
- Quickstart
- Setting up your credentials
- Adding your first playlist
- Using alembic day to day
- Updating alembic
- Troubleshooting
What alembic does
Give alembic a Spotify playlist link, and from then on it will:
- Download every track in the playlist automatically, over Soulseek
- Tag each file properly (artist, title, album, genre) using Spotify's own metadata, so your library looks clean instead of a pile of "Track 01.mp3" files
- Import everything into your music library, ready to show up in Navidrome or any other music server that reads a normal folder-based library
- Rebuild the playlist as a real M3U file, so it plays back in the right order in your music app
- Keep checking on a schedule, so when you add new songs to the Spotify playlist, they show up in your library on their own
- Show you exactly what's downloaded and what's still waiting, side by side with the original Spotify tracklist, per playlist
- Find duplicate copies of songs (same track downloaded twice, a different format, or the same recording under different tags) and let you review and clean them up, nothing gets deleted without your say-so
- Let you fix things by hand when needed: correct a tag, re-tag a track from a URL, manually import a file you dropped in yourself, or delete something you don't want
It also keeps a dashboard showing what's in your library, what got downloaded recently, the history of every automated job it's run, and a quick check that your traffic is actually going through a VPN if you've set one up.
Everything runs in one Docker container. There's no separate database to install and no cron jobs to hand-edit, alembic manages all of that internally.
What you need before you start
alembic is built to slot into a home server that already has a few things running. It is not a starting point for a music server from scratch, it's the automation layer on top of one.
Before you begin, make sure you have:
- A server that runs Docker, with
docker composeavailable. Any Linux box, NAS, or mini PC works. - A music server that reads a plain folder of tagged files, such as Navidrome. alembic builds your library on disk; Navidrome (or similar) is what you actually listen with.
- A Soulseek account. This is how tracks get downloaded. Sign up free at soulseek.com.
- A free Spotify Developer app. This lets alembic read playlist contents (it does not download from Spotify itself, only Soulseek). Takes two minutes, see the credentials section below.
- A login provider that speaks OpenID Connect (OIDC). alembic doesn't have its own username/password login, it delegates to something you already trust. Pocket ID is a small self-hosted option built exactly for this, but Authentik, Keycloak, Authelia, or any other OIDC provider works too.
Optional, add these later if you want them:
- A Bandcamp account, if you want to also sync your Bandcamp purchases into the library automatically.
- AzuraCast or Qobuz credentials, if you use either of those.
- A Telegram bot, if you want job notifications sent to you.
- A VPN container (such as gluetun), if you want Soulseek's network traffic routed through a VPN. Not required to get started.
Quickstart
1. Get the image
Clone this repository onto your Docker host and build the image locally:
git clone <this-repo-url> alembic
cd alembic
docker build -t alembic:latest .
This takes a few minutes the first time. You don't need to touch anything inside the app/ or pipeline/ folders, the Dockerfile handles all of it.
2. Create your folders
alembic needs three places to store things:
- A music folder, where your actual songs live. If you already have a music library, point at it directly.
- A config folder, where alembic keeps its own database, your saved credentials (encrypted), and logs.
- A secrets folder, which holds just one file: the master key described in the next step. This is kept separate from the config folder on purpose (see below).
mkdir -p /path/to/your/music
mkdir -p /path/to/alembic-config
mkdir -p /path/to/alembic-secrets
alembic runs inside the container as a non-root user (user and group id 1000). If you created these folders as yourself or as root, the container will not be able to write to them and will stop on startup with a clear message telling you this. Give that user ownership of the config and music folders:
sudo chown -R 1000:1000 /path/to/alembic-config
sudo chown -R 1000:1000 /path/to/your/music
Everything about how alembic runs (which playlists you follow, your credentials, scheduling) is stored inside the config folder, so moving alembic to a new machine is a matter of copying it across. Back up the secrets folder separately, and do not keep it in the same backup as the config folder. The reason is in the next step.
3. Generate a master key
alembic encrypts every credential you give it (Spotify keys, Soulseek password, and so on) before storing it in the config folder. It needs a key to do that:
openssl rand -base64 32 > /path/to/alembic-secrets/master.key
chmod 600 /path/to/alembic-secrets/master.key
Keep this file safe. If you lose it, alembic can no longer read your saved credentials and you'll need to re-enter them.
Keep it separate from your config folder, which is why it goes in its own folder above. The config folder holds the encrypted credentials. If the key sat in there too, then anyone who got hold of one backup would have both the locked box and its key, and the encryption would not protect you. Storing the key on its own keeps the two apart.
4. Set up login (OIDC)
Register alembic as an application in your OIDC provider (Pocket ID, Authentik, whichever you use). You'll need to tell it:
- Redirect URI:
https://<wherever-you-host-alembic>/auth/callback - Scopes:
openid profile email
In return, it will give you three values you'll need in the next step:
- An issuer URL (something like
https://id.example.com) - A client ID
- A client secret
5. Write your docker-compose file
Create a docker-compose.yml next to your alembic folder:
services:
alembic:
image: alembic:latest
container_name: alembic
ports:
- "8420:8420"
environment:
- POCKETID_ISSUER=https://id.example.com
- POCKETID_CLIENT_ID=your-client-id
- POCKETID_CLIENT_SECRET=your-client-secret
- SESSION_SECRET=some-long-random-string
# Optional: lock login to a single email, recommended for a personal server
- ALLOWED_EMAIL=you@example.com
volumes:
- /path/to/your/music:/data/music
- /path/to/alembic-config:/config
secrets:
- alembic_master_key
restart: unless-stopped
secrets:
alembic_master_key:
file: /path/to/alembic-secrets/master.key
A few notes on the environment values:
SESSION_SECRETsigns your login session. You can leave it out and alembic will generate one on first start and save it inside the config folder for next time. If you would rather set it yourself, use any long random string, for example fromopenssl rand -base64 32.ALLOWED_EMAILis optional but recommended. Even if your OIDC provider only has your account today, this makes sure only that one email can ever log in.- If you want to route Soulseek traffic through a VPN container instead of exposing alembic's port directly, see the comments in
docker-compose.snippet.ymlin this repo for thenetwork_mode: "service:<vpn-container>"pattern. Most people can skip this and use the simple port mapping above.
6. Start it up
docker compose up -d
On first boot, alembic seeds a few default config files into your config folder and sets up its internal database automatically. Check that it started cleanly:
docker compose logs -f alembic
You should see Uvicorn running on http://0.0.0.0:8420 with no errors.
7. Log in
Open http://<your-server>:8420 in a browser. You'll be redirected to your OIDC provider to log in, then dropped onto the alembic dashboard.
Setting up your credentials
Once logged in, go to Settings → Credentials and fill in each service you plan to use. Nothing here is required all at once, add them as you need them. Values are write-only: once saved, they're encrypted and never shown back to you in the browser (you'll just see a "Configured" badge). Each service also shows a one-line description right on the page of what it's actually used for.
Spotify, Soulseek, and Navidrome are marked Required — every playlist sync depends on them, so there's no way to turn them off. Bandcamp, AzuraCast, Qobuz, and Telegram are optional and each has an on/off switch: turning one off pauses whatever it drives (its scheduled sync, or just not being used for buy-link lookups) without deleting the credential you already saved.
| Service | What to enter | Where to get it |
|---|---|---|
| Spotify (required) | Client ID, Client Secret | Create a free app at developer.spotify.com/dashboard. You don't need any special access, the basic free tier is enough to read playlist contents. |
| Soulseek (required) | Username, Password | Your normal Soulseek login. |
| Navidrome (required) | Base URL, admin username, admin password | The address of your Navidrome server and an admin account on it. Used so alembic can trigger a library rescan after downloads. |
| Bandcamp (optional) | Username, format preference, cookies | Export your Bandcamp cookies while logged into bandcamp.com in your browser, using an extension like "Get cookies.txt LOCALLY", and paste the contents in. Format preference is a space-separated list like flac mp3-320. |
| Qobuz (optional) | Token, App ID, region | A logged-in Qobuz session token — used (alongside Bandcamp) as a source for "buy this" links stamped onto tracks you didn't purchase there. |
| AzuraCast (optional) | API key | Only relevant if you also run an AzuraCast radio station off the same library. It's not a buy-link source itself (that's Bandcamp/Qobuz, written straight to the file); this just lets alembic tell AzuraCast to immediately pick up a tag change instead of waiting for its own periodic scan. |
| Telegram (optional) | Bot token, chat ID | Create a bot via @BotFather if you want the daily status digest sent to a chat. |
Adding your first playlist
- Go to Playlists → Add playlist.
- Paste the Spotify playlist URL and give it a name (this name becomes the folder/tag name in your library).
- Pick a daily sync time.
- Save.
That's it. On its next scheduled run (or immediately, using the Run now button on the playlist's page), alembic will download every track in the playlist, tag it, import it into your library, and build a matching .m3u8 playlist file.
Using alembic day to day
- Dashboard — a snapshot of your library: track counts, formats, what got downloaded in the last 24 hours, recent job activity, and a public-IP check (handy if you route alembic's traffic through a VPN container and want to confirm it's actually taking effect).
- Playlists — add, pause, or remove synced Spotify playlists. Each playlist's page shows the original Spotify tracklist side by side with a "Downloaded" / "Waiting to download" breakdown, so you can see exactly what's made it into your library and what hasn't.
- Library — browse and search everything in your library. Fix tags on a track, re-tag it from a URL, or delete it entirely.
- Import — drag a file onto the dropzone (or click to browse) to add it to your import folder, then import it individually or all at once, with an optional playlist tag.
- Dedup — click "Scan now" to check for duplicates both by filename/tags and by matching the actual audio (catches the same recording filed under different names). Nothing is ever deleted automatically: you approve each deletion, or mark a pair as "keep both" if it's not actually a duplicate.
- Genres — preview what a genre refresh from Spotify would change, then apply it, or lock an artist's genre so it's never overwritten again.
- Settings — three sub-pages: Credentials (see above), Jobs (every scheduled task, enable/disable each one, run it on demand, and see the history and logs of every past run), and Artist Casing (a list of canonical artist name spellings, e.g. "Mall Grab" not "MALL GRAB", so the same artist doesn't end up split across differently-cased folders).
Updating alembic
Pull the latest code, rebuild, and restart:
cd alembic
git pull
docker build -t alembic:latest .
docker compose up -d --force-recreate alembic
Your library, credentials, and settings all live in your mounted folders, so updating the image never touches your data.
Backups
Everything worth keeping is in the folders you already created:
- The config folder holds the app database, the beets library database, your rendered settings, and your encrypted credentials. Backing this up captures your playlists, schedules, and history.
- The secrets folder holds
master.key. Back this up too, but keep it in a separate backup from the config folder. The config folder holds your credentials in encrypted form, and the key is what unlocks them. Keeping the two apart means one leaked backup is not enough to read your credentials. - The music folder is your library itself. Back it up however you already back up files.
To restore on a new machine: put the config and secrets folders back where they were, point your compose file at them, and start the container. If you ever lose master.key, alembic can still start, but it can no longer read your saved credentials, so you would re-enter them once under Settings then Credentials.
Advanced settings
Most people never touch these. They are environment variables you can add to your compose file's environment: block.
TZsets the timezone used for scheduling and for times shown in the app, for exampleTZ=America/Toronto. Defaults to UTC. This is the standard Docker timezone variable, so if you already set it for other containers, alembic follows it too.ALLOWED_EMAILlocks login to a single email address (recommended for a personal server).MUSIC_DATA_DIR,ALEMBIC_CONFIG_DIR,ALEMBIC_PORT, andENCRYPTION_KEY_FILElet you change the in-container paths and port. The defaults match everything in this README, so you only need these for unusual setups.
You can confirm your setup at any time by visiting /setup in your browser (no login needed), which lists what is configured and what is missing. There is also a /health endpoint that returns the same information as JSON for uptime monitors.
Migrating from the older script version
If you used the earlier version of this project, when it was a set of scripts rather than this container, there is a short guide for bringing your old playlists and credentials across in docs/MIGRATION.md.
Troubleshooting
Something isn't working and I'm not sure what.
Open /setup in your browser. It checks the things a fresh install commonly gets wrong (login not configured, master key missing, config folder not writable) and tells you exactly what to fix. You do not need to be logged in to see it.
The container won't start / exits immediately.
Check the logs with docker compose logs alembic. The most common cause is folder ownership: the container runs as user id 1000, so the config and music folders must be owned by it. Run sudo chown -R 1000:1000 <folder> on each, as described in step 2.
I can't log in / get redirected in a loop.
Double check the redirect URI registered with your OIDC provider exactly matches https://<your-host>/auth/callback, including http vs https.
A playlist says it's synced but nothing downloaded. Check that playlist's job history under Settings → Jobs, the log will usually show whether Soulseek couldn't find a track, or a credential is missing. The playlist's own page will also show everything as "Waiting to download" if nothing came through.
Dedup found something that isn't actually a duplicate. Use the "Keep both" button on that pair. alembic will remember your decision and won't flag that exact pair again.
I dropped files in the import folder and they're not showing up. Go to the Import page and confirm you don't have leftover files that failed a previous import, check that page's status for any error before re-running.