Migrating Our Music from Subsonic to Gonic
We have _almost_ always self-hosted our music collection and, for a little over a decade now, have been streaming music from a self-hosted instance of Subsonic.
However, that install is now _positively ancient_ : the most recent release of Subsonic was cut in November 2019 and is susceptible to things like the log4j vulnerability (I mitigated by locking down access at the reverse proxy).
I've periodically looked for alternatives but (until recently) have never quite found anything that we were able to make the jump to. Ever since Subsonic went closed source, there have been various forks (Airsonic _et al_), but few seem to have stayed in development for very long (and are all, of course, heavy Java apps).
Recently, though, I stumbled across Gonic.
Rather than being a fork, Gonic is a Subsonic _compatible_ server, which meant that I could still (more or less) just point our existing players at it.
This post talks about migrating our music collection from Subsonic to Gonic.
* * *
#### History of Our Music
The timeline of our digital music collection looks something like this:
I think that there was also a brief period where I was using Realplayer (RealOne at the time) instead of the original Windows Media Player.
Essentially, our collection started off as a bunch of locally stored audio files, courtesy of ~~Kazaa~~ ~~Limewire~~ ripping CDs. Our short stint of having to use (the _awful_) SonicStage for our MP3 players is best left buried in the sands of time.
As time went by, we moved from playing files stored on the same system to streaming those same files over a network - first from Google Play Music and then from a self-hosted Subsonic instance.
I never _quite_ got onto the Spotify bandwagon: I tried (and liked) it when they first came to the UK, but was put off by them requiring a paid subscription to be able to use it on Linux (and/or my phone)1.
* * *
### Alternatives
There are a _bunch_ of self-hosted music solutions out there, but the following came particularly close to being chosen.
* * *
#### Navidrome
I first looked at Navidrome about a year ago.
Navidrome doesn't use a folders heirachy and, instead, organises the collection by looking at how tracks are tagged (there are no plans to change this).
This seemed like a reasonable position to take, but after experimenting, I found that our collection needed more work than I was willing or able to invest to straighten all of the tags out (I kept running into annoying edge cases, like some tracks having `Album Artist` set to the name of the production company).
It's a pity, because I really like the interface (have a look at the demo site, it's nice!).
* * *
#### Funkwhale
Funkwhale is Fediverse enabled, meaning that users can also explore music that others have shared. I _really_ like the idea and, so, was quite drawn to it.
There are Funkwhale instances with open registration, but I didn't want to be tied to someone else's infra: the whole point is that our collection should always be under _our_ control.
Although it's possible to self-host Funkwhale, it has some infra level dependencies - at a minimum it needs Postgres and Redis. I've been working to get Postgres _back out_ of my LAN, so didn't really want to add another dependency on it.
* * *
### Gonic
After much exploration, I settled on Gonic.
Unlike some of the other options its web interface is quite simple and does not let you explore or play media:
I've a vague feeling that, when I was looking last year, I dismissed Gonic because it felt like a big step back from the functionality provided by Subsonic's interface:
However, it turned out that the only person who actually uses that interface is _me_. Even then, I only really use it to trigger library updates (which Gonic's interface _does_ support).
Actual playback primarily occurs via the following:
* Jamstash
* Dsub
* My music kiosk
Most importantly, Gonic exposes a Subsonic compatible API so we could continue to use the same players.
* * *
#### Deployment
Gonic is written in Go, so can easily be compiled and run natively. However, I now tend to deploy containers (primarily because it makes it easier to lift & shift between hardware).
I started by creating directories to use as persistent storage
mkdir gonic
mkdir gonic/playlists
mkdir gonic/cache
mkdir gonic/podcasts
mkdir gonic/data
I _didn't_ create a directory for music: that lives on an NFS share which was already mounted elsewhere on the host (the `subsonic` container lived on the same box).
I added the following to my `docker-compose.yml`:
gonic:
restart: always
image: sentriz/gonic:latest
container_name: gonic
ports:
- 4747:80
environment:
- GONIC_SCAN_AT_START_ENABLED=true
- GONIC_SCAN_INTERVAL=1440
- GONIC_MUSIC_PATH=/mnt/Music-NAS/Albums_Sorted
volumes:
- /mnt/Music-NAS/:/mnt/Music-NAS:ro
- /home/ben/docker_files/gonic/data:/data
- /home/ben/docker_files/gonic/playlists:/playlists
- /home/ben/docker_files/gonic/cache:/cache
- /home/ben/docker_files/gonic/podcasts:/podcasts
There's quite an important note here: by default, Gonic expects to find music under `/music`, but I've overridden that.
I _could_ have bound `/mnt/Music-NAS` to `/music` (in fact, I originally _did_) but the prefix became quite important when importing playlists from Subsonic (more on that below).
I started the container:
docker compose up -d
Gonic's web interface came up almost immediately and, in the background, `gonic` began scanning our music library.
While waiting for the scan to finish, I changed the admin creds (the default is `admin`/`admin`) and created an unprivileged user.
After logging in as my user, I hooked Gonic up to Last.fm and ListenBrainz:
This means that the server will automatically scrobble - something that, apparently, I haven't done since 2010!2
In order to provide external players with access, I configured my reverse proxy to point to Gonic and acquired a SSL cert from LetsEncrypt:
upstream gonic {
server 192.168.11.145:4747 weight=1;
keepalive 5;
}
server {
listen 443 ssl;
server_name gonic.example.com;
root /usr/share/nginx/letsencryptbase;
index index.php index.html index.htm;
ssl_certificate /etc/letsencrypt/live/gonic.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/gonic.example.com/privkey.pem;
ssl_session_timeout 5m;
location / {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_pass http://gonic;
# Force use of upstream keepalives
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_redirect http:// https://;
proxy_buffers 16 16k;
proxy_buffer_size 16k;
add_header X-Clacks-Overhead "GNU Terry Pratchett";
add_header Strict-Transport-Security "max-age=31536000; includeSubdomains; preload" ;
}
}
Once the library index had finished (it took about 10 minutes), I reconfigured Dsub on my phone to point to the Gonic server and attempted to play some music - it worked!
* * *
#### Playlist Import
Although our music collection was there, our playlists still needed copying across.
Subsonic doesn't expose a way to bulk export playlists, however the web interface does provide a way to do them one by one.
If you click into a playlist there's an `Export` option at the top:
Clicking this downloads a m3u8 format playlist.
I had worried that playlists might rely on Subsonic specific track IDs, but they turned out to be _much_ simpler than that:
#EXTM3U
/mnt/Music-NAS/Albums_Sorted/Semblant/Obscura (2020)/03 - Dethrone the Gods, Control the Masters (Legacy of Blood, Pt. IV).mp3
/mnt/Music-NAS/Albums_Sorted/Semblant/Lunar Manifesto/01 Incinerate.mp3
/mnt/Music-NAS/Albums_Sorted/Semblant/Lunar Manifesto/02 Dark of the Day.mp3
/mnt/Music-NAS/Albums_Sorted/The Agonist/Prisoners (2012)/05. Panophobia.mp3
/mnt/Music-NAS/Albums_Sorted/Alien Weaponry/Tu/05 Kai Tangata.mp3
/mnt/Music-NAS/Albums_Sorted/Arch Enemy/Will To Power (2017)/06. Reason to Believe.mp3
/mnt/Music-NAS/Albums_Sorted/Arch Enemy/Will To Power (2017)/04. The World Is Yours.mp3
**This** is why I needed to recreate my `gonic` container with music mounted under `/mnt/Music-NAS`.
Technically, I could have used `sed` (or similar) to replace the path prefix, but I was migrating enough playlists that I didn't want to _have_ to process them if it could be avoided.
To import the playlists into `gonic`, I needed to know `gonic`'s ID for the user that would own them.
There are two ways to figure this out
1. Count on your fingers - the default `admin` user is `1`, the next is `2` etc
2. Query gonic's database
The first is easier, but doesn't _quite_ scratch the geek itch:
sqlite3 gonic.db 'select id, name from users'
1|admin
2|ben
Note: if you're setting up `gonic` yourself, don't copy this database to random systems - `gonic` stores credentials in plaintext :(
Once I knew the user ID, I just needed to create a directory with the same name and move the playlists into it:
mkdir gonic/playlists/2
mv ~/playlists/*.m3u8 gonic/playlists/2
I didn't even need to restart `gonic`, the playlists appeared in my player almost immediately and took their names from the playlists's filename.
There was, however, one last thing to do.
Some of my Subsonic playlists were made available to other users on the server:
`gonic` _also_ supports this, but it wasn't immediately clear how to enable it.
After fiddling around in DSub, I found an `Update Information` option which allowed me to mark the playlist as shared with others.
After I'd done so, I opened the playlist file in `less` - new metadata had been added to the top:
#EXTM3U
#GONIC-NAME:"Heavy mix"
#GONIC-COMMENT:""
#GONIC-IS-PUBLIC:"true"
/mnt/Music-NAS/Albums_Sorted/Semblant/Obscura (2020)/03 - Dethrone the Gods, Control the Masters (Legacy of Blood, Pt. IV).mp3
/mnt/Music-NAS/Albums_Sorted/Semblant/Lunar Manifesto/01 Incinerate.mp3
Although I could have edited the relevant playlists, it seemed quicker (and less error prone) to use Dsub, so I worked through the (relative few) playlists that needed sharing and updated the setting.
* * *
### Star Syncing
In hindsight, I'm not overly convinced that I _needed_ this service.
The author of `gonic` has a second repo: `gonic-lastfm-sync`. This contains a small codebase which provides bi-directional syncing of "favourites" (or stars, depending on the player) between `gonic` and `last.fm`.
So, if I previously favourited something in `last.fm`, that state could be pulled down.
The repo contains a `Dockerfile`, but I wanted something a bit lighter and so wrote my own:
FROM cgr.dev/chainguard/wolfi-base AS builder
RUN apk add go git \
&& mkdir /build \
&& cd /build \
&& git clone --depth=1 https://github.com/sentriz/gonic-lastfm-sync.git \
&& cd gonic-lastfm-sync \
&& go build -o lastfm-gonic-sync .
FROM cgr.dev/chainguard/wolfi-base
COPY --from=builder /build/gonic-lastfm-sync/lastfm-gonic-sync /bin/
ENV GONIC_DB_PATH /data/gonic.db
CMD ["sh", "-c", "while true; do lastfm-gonic-sync; sleep 3600; done"]
Once I'd built the image, I added a section to my `docker-compose.yml` to run it, passing it the path to the `gonic` container's database:
gonic_sync:
restart: always
image: gonic-lastfm-sync:b0d860
container_name: gonic-lastfm-sync
environment:
- GONIC_GONIC_USERNAME=ben
volumes:
- /home/ben/docker_files/gonic/data:/data
I brought the container up:
docker compose up -d
The software went to work.
In practice, though, it turned out that I'd only actually previously starred a few things anyway:
$ docker logs -f gonic-lastfm-sync
2025/12/01 17:36:12 no match for "dreamtheaterthisdyingsoul"
2025/12/01 17:36:12 saved lastfm->gonic stars, 7 of 8 matched
2025/12/01 17:36:12 saved gonic->lastfm stars, 0 new
Ah well
* * *
### Cutover
The original plan was for _me_ to use `gonic` for a while and then cut the rest of the family over once I was happy that things were working. But, everything seemed to be OK.
In Dsub, I deleted my temporary profile and then updated the URL of my existing Subsonic profile to see whether profile reuse caused any issues: it didn't.
I couldn't _really_ think of anything else that needed checking, so, I decided to take a risk and just cut everyone over.
To prepare for the change, I need to create user accounts in `gonic` with the same credentials as those used for Subsonic.
I didn't have those credentials, but Subsonic _did_ : within it's data directory is a file called `subsonic.script` - this is essentially a bunch of SQL statements that Subsonic uses to recreate it's database at startup.
I used `grep` to extract the statements used to populate user records:
grep "INSERT INTO USER VALUES" subsonic.script
This returned lines like this:
INSERT INTO USER VALUES('admin','enc:6e65766572676f6e6e6167697665796f757570',149828415,0,0,FALSE,NULL)
From this, I needed two things: the username and a decoded password.
Although not in ASCII, Subsonic _does_ store credentials in the clear - they're only hex encoded and so can be decoded using `xxd`:
grep "INSERT INTO USER VALUES" subsonic.script | cut -d"(" -f2 | cut -d, -f1,2 | while read -r line
do
username=`echo "$line" | cut -d, -f1 | tr -d "'"`
password=`echo "$line" | cut -d, -f2 | tr -d "'" | cut -d: -f2 | xxd -r -p`
echo "${username}:${password}"
done
With this information in hand, I:
* Created corresponding users in `gonic` with the same passwords as in Subsonic
* Logged into their subsonic accounts and exported playlists
* Imported playlists into `gonic`
Once I was ready, I updated my reverse proxy for the Subsonic domain so that it would proxy onto `gonic` instead.
proxy_pass http://gonic;
One Nginx reload later, nothing broke.
* * *
### Bonus: A New Desktop Player
Although the migration was transparent to the _other_ users of my Subsonic instance, it did change things for me.
It was no longer possible to log into a web interface to manage our music collection and I now needed to do so through a front-end app.
DSub is perfectly capable, but I'm not a fan of **any** app-only workflow (_looks pointedly towards the finance sector_), if I don't have the option of doing it from a desktop, I'm not doing it _at all_.
Jamstash is great for _playing_ music but isn't really suited to _managing_ it. Whatever I landed on would, obviously, need to be able to talk to a Subsonic compatible API.
After a bit of looking around, I stumbled across Feishin:
It has _pretty much_ everything that I'm likely to need and, after years of keeping a browser tab open, there's something quite refreshing about going back to having a dedicated music app (even if, being an electron app, it is actually still just a wrapper around a browser).
Although I haven't played around with it yet, it also seems that Amarok is back in development!3
* * *
### Resource Usage
Back when first deploying Subsonic, I added the following comment to my ticket:
It should come as no surprise that a Go program is more RAM efficient than an aged Java beast, but _just look at the difference_ :
Even idle, Subsonic still requires _ten times_ the memory demanded by `gonic` (admittedly, I'm not sure that half a gig of RAM constitutes "RAM hungry" nowadays).
CPU usage graphs tell a similar story:
That brief spike in usage appears to be `gonic` scanning our music library for updates.
* * *
### Conclusion
Because of my experiences with Navidrome last year, I expected that this was going to be a big, long running project.
That hasn't proven to be the case though - in fact, it's taken me substantially longer to write the first draft of this blog post than it did to perform the full migration.
The move was entirely transparent to my Subsonic users and our music is now served by an _extremely_ lightweight container without any of the overhead associated with Java apps.
I've now been able to tear the Subsonic container down, greatly reducing the average age of the software running on our LAN, let alone that which is exposed to the internet.
* * *
1. We were skint, so I couldn't really afford/justify a subscription. But, they also lost the future me that _could_ afford to pay for subscriptions. ↩
2. Which, I think, _probably_ puts a date on when we moved from Amarok to Google Play Music ↩
3. Though it's not a given that the Subrok plugin will still work with it - the plugin was a bit long in the tooth a decade ago and seems to have been abandoned since. ↩
4. This was actually a drop from when I first looked - it had been closer to 800MiB the day before ↩