Black Candy and the container that forgets your sessions
A self hosted music streaming server
At a glance
- What is it?
- Black Candy is a Ruby on Rails music streaming server shipped as a single container image, aimed at people who already own a music library and want to stream it from their own hardware. The install is one command; the operational details are where it gets interesting, including a storage path in the README's own compose example that does not match the persistence instructions.
- Who is it for?
- Adopt Black Candy if you keep a local music folder, want no account system beyond your own, and are willing to own the upgrade step by hand. Do not adopt it if you want zero filesystem work, because data only persists if you mount /rails/storage.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 24 days ago.
- What is it written in?
- Mainly Ruby, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on November 14, 2026, and from our analysis. They are not legal advice.
Star history
- blackcandy-org/blackcandy4,434 ★ · +23
| Repository | 2026-09-11 | 2026-11-14 |
|---|---|---|
| blackcandy-org/blackcandy | 4.4k | 4.4k |
23 Stars in the last 64 days, measured from the daily GitHub snapshot. Compare with other projects
Editorial analysis
A media server for a library you already own, not a catalogue service
Black Candy does one job: it streams the music files sitting on your disk to a browser or a phone. There is no catalogue to browse, no recommendation engine, no editorial content. You point it at a directory, it indexes what is there, and it serves it. That framing matters more than the feature list, because the whole design assumes a single household or a single owner rather than a public service with sign-up, quota and moderation.
The audience is narrow and obvious. Someone with a few terabytes of FLAC files who has retired or never adopted a commercial streaming account and wants playback on a phone over their own network. The project ships first-party mobile apps for the App Store, Google Play and F-Droid, plus an Android APK on GitHub Releases, which tells you the intended shape of use: your server at home, the phone as the remote control.
One thing the README is careful about: the demo instance at demo.blackcandy.org logs in with [email protected] and password foobar, and that demo user has no administrator privileges. The note under it says the audio comes from Free Music Archive. So the public demo cannot show you the library management half of the product. You are evaluating this by reading the docs, not by clicking around.
Rails 8, Hotwire and a container with no tag to pin it to
The primary language is Ruby, and the Dockerfile names its toolchain: ruby:4.0.2-alpine as the base image, node:20.11.0-alpine for the asset build, and assets:precompile run with SECRET_KEY_BASE_DUMMY=1 in the builder stage. That dummy value gives the asset pipeline something to sign with at build time, which is why your own key only matters once the container runs.
The front end is Hotwire rather than a separate SPA. package.json lists @hotwired/turbo-rails, @hotwired/stimulus and @hotwired/hotwire-native-bridge, and the Dockerfile deletes app/javascript/* after compiling, so the browser assets are baked into public/. Howler handles audio playback. The native bridge is the interesting piece: it is how the same Turbo pages talk to the mobile shells.
The final image sets LD_PRELOAD to jemalloc and ships ffmpeg and vips, then runs as uid 1000 through gosu. It listens on port 80 inside the container and starts with CMD ["./bin/thrust", "./bin/rails", "server"], which is why every docker example in the README maps 80 to a host port and the compose file publishes 3000 instead. Nothing in the install instructions pins a tag, so the container you start is whatever image currently carries the latest tag.
One docker run, then the part the README calls That's all
Installation is a single command, taken verbatim from the README:
docker run -p 80:80 ghcr.io/blackcandy-org/blackcandy:latest
# Or pull from Docker Hub.
docker run -p 80:80 blackcandy/blackcandy:latestOpen http://localhost or the host IP, and log in with the initial admin credentials, [email protected] and password foobar. Because nothing was mounted, that first container has no state worth keeping. Your library has to be attached:
docker run -v /media_data:/media_data -e MEDIA_PATH=/media_data ghcr.io/blackcandy-org/blackcandy:latestNow generate a session secret and set it explicitly:
openssl rand -hex 64The README states the consequence of skipping this plainly. If SECRET_KEY_BASE is not set, Black Candy generates a new one on each startup, which invalidates all existing sessions. On a bare docker run with no volume and no environment variable, that means you are logged out after every restart, on top of losing everything else.
The compose example mounts /app/storage, the persistence section says /rails/storage
Here is the inconsistency worth catching before you deploy. The persistence section states that all data needing to persist is stored in /rails/storage, and its example mounts a host directory there:
mkdir storage_data
docker run -v ./storage_data:/rails/storage ghcr.io/blackcandy-org/blackcandy:latestThe compose example in the same README mounts a named volume somewhere else:
services:
app:
image: ghcr.io/blackcandy-org/blackcandy:latest
restart: unless-stopped
ports:
- "3000:80"The WORKDIR in the Dockerfile is /rails, and the compose example mounts the named volume storage_data at /app/storage. Those two paths are not the same location inside the container. Copy the compose example as given and your persisted data does not land in the directory the persistence section told you to back up. The per-run docker example with /rails/storage is the one that matches the image layout. Verify which path your container actually writes to before you trust either document.
One more sharp edge in the same section: host and container users disagree by default, so the README tells you to pass your own uid and gid with --user.
SQLite by default, PostgreSQL behind three URLs
Black Candy stores its database in SQLite unless told otherwise. The reasoning given is that SQLite simplifies installation and suits a small self-hosted server. For one household that holds, and it means you can run the whole thing without standing up a second service.
The switch is two environment variables, and the README warns that three are actually required. DB_ADAPTER and DB_URL point at the database, while CABLE_DB_URL covers the Pub/Sub database and QUEUE_DB_URL the background job database:
docker run -e DB_ADAPTER=postgresql -e DB_URL=postgresql://yourdatabaseurl ghcr.io/blackcandy-org/blackcandy:latestSet DB_ADAPTER to postgresql and provide DB_URL alone and you have not actually configured the deployment, because the table lists CABLE_DB_URL and QUEUE_DB_URL as required whenever PostgreSQL is in use. Set both of those variables before starting, because the one-line example in the README is an illustration of the adapter, not a complete configuration.
Upgrading is a destructive step the README flags twice
There is no in-place upgrade. The container is stopped, removed, and recreated from a freshly pulled image:
docker stop <your_blackcandy_container>
docker rm <your_blackcandy_container>
docker pull ghcr.io/blackcandy-org/blackcandy:latest
docker run <OPTIONS> ghcr.io/blackcandy-org/blackcandy:latestNotice that the replacement run is written as docker run <OPTIONS>, not as a copy of your original command. Every flag you relied on has to be re-supplied from memory or from notes. If you followed the per-run examples above instead of using compose, that means MEDIA_PATH, SECRET_KEY_BASE and any DB variables all have to be reconstructed by hand, and getting SECRET_KEY_BASE wrong logs you out.
The README attaches an IMPORTANT block to this section saying you need to read the upgrade guide carefully first, because a new version may contain breaking changes, and it links docs/upgrade.md. Read the guide before pulling, not after your container is gone.
Where Funkwhale and Gonic diverge from Black Candy
The closest comparison on approach is Funkwhale. Funkwhale is built around a federated ActivityPub model: instances can follow each other and share libraries across the network. Black Candy reads a directory, it serves it, and the trust boundary is your own filesystem. If you want a shared catalogue that accumulates artists from elsewhere, Funkwhale's model is the different one. If you want your own files and nothing else, Black Candy's is simpler and has no concept of a remote peer to misconfigure.
Gonic, by contrast, is a server with a narrower surface: music over the Subsonic API, so any compatible client can connect. Black Candy ships its own clients and its own Hotwire pages. So the real axis is ownership. Gonic is a protocol endpoint for third-party clients. Black Candy is a complete product with its own front end, where the apps and the server are developed together.
Gaps that sit on the operations path, and who owns the music
The MIT licence covers the server software only. Copyright on the recordings in your MEDIA_PATH stays with whoever owns those files, and streaming a library for your own listening is a separate question from the terms of the binary serving it.
The Docker Hub mirror sits alongside the GitHub Container Registry image, so a ghcr.io outage or rate limit leaves you a second pull path rather than a problem to solve.
Editorial conclusion
Adopt Black Candy if you keep a local music folder, want no account system beyond your own, and are willing to own the upgrade step by hand. Do not adopt it if you want zero filesystem work, because data only persists if you mount /rails/storage. Before anything else, check whether /rails/storage is mounted and SECRET_KEY_BASE is set on your deployment: without SECRET_KEY_BASE every startup generates a new one and invalidates existing sessions, and the README's compose example mounts its volume at /app/storage rather than /rails/storage.
Frequently asked questions
What is Black Candy?
Black Candy is a self-hosted music streaming server written in Ruby on Rails, described as your personal music centre. It runs as a container image and is intended for people who already own a music library.
How do I install Black Candy?
The README gives a single docker run command against the ghcr.io/blackcandy-org/blackcandy:latest image, or the blackcandy/blackcandy:latest mirror on Docker Hub. You then reach http://localhost or the host IP and log in with the initial admin account.
How do I add my own music to Black Candy?
Mount a host media directory into the container and set the MEDIA_PATH environment variable to that path inside the container. The README shows mounting /media_data on the host to /media_data in the container and setting MEDIA_PATH accordingly.
Why do I get logged out every time the container restarts?
SECRET_KEY_BASE is the basis for the secrets protecting sessions and other sensitive data. If it is not set, Black Candy generates a new one on each startup, which invalidates all existing sessions, so set it to your own unguessable value.
Can Black Candy use PostgreSQL instead of SQLite?
Yes. Black Candy uses SQLite by default because it simplifies installation, and PostgreSQL is available by setting DB_ADAPTER and DB_URL. The environment variable table also marks CABLE_DB_URL and QUEUE_DB_URL as required whenever PostgreSQL is used.
How do I upgrade Black Candy?
Upgrading means pulling a new image, then removing the old container and creating a new one, or using docker-compose down, docker pull and docker-compose up. The README warns there may be breaking changes and points to the upgrade guide in docs/upgrade.md.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/blackcandy-org-blackcandy)Star history chart for your README
[](https://hysenlabs.com/projects/blackcandy-org-blackcandy)Was this useful?