Oct 31, 2025 Updated on Nov 1, 2025
Introduction
I have been paying for Spotify for a long time until this summer. I thought it was the right thing to do since some of the bands I listen to are not “mainstream”, so pirating them straight up would be straight up being an a-hole. But recently there has been on going issues with spotify on how they treat artists, and I have been wanting to get into this “hobby” for a while now, so I decided to jump the boat and just go for it. Recently I have been seeing more posts about self hosting music on r/selfhosted, so I have decided to share my setup with the world on how to automate music organization, for a low resource system.
D…
Oct 31, 2025 Updated on Nov 1, 2025
Introduction
I have been paying for Spotify for a long time until this summer. I thought it was the right thing to do since some of the bands I listen to are not “mainstream”, so pirating them straight up would be straight up being an a-hole. But recently there has been on going issues with spotify on how they treat artists, and I have been wanting to get into this “hobby” for a while now, so I decided to jump the boat and just go for it. Recently I have been seeing more posts about self hosting music on r/selfhosted, so I have decided to share my setup with the world on how to automate music organization, for a low resource system.
Disclaimer: I highly suggest you to support your favourite artists in some way instead of straight up pirating or using Spotify. This could be buying merch or purchasing CDs or Vinyls.
Overall structure
This guide combines following technologies on the server
- docker and compose as container runtime
- traefik as application proxy
- slskd for Soulseek
- gonic as subsonic server
- wrtagweb for music tagging and organization
And for the clients
- Symfonium for android
- Supersonic for MacOS
You can pick and choose your client however you like. Take a look at Selfhosted music overview
---
config:
theme: 'neutral'
---
flowchart TD
A[Web Browser]
B[Slskd]
C[Gonic]
D[Wrtagweb]
E[Symfonium]
F[Supersonic]
A -->|Search and Download| B
B -->|DownloadComplete: /op/move| D
D -->|Complete: sync| C
E --> C
F --> C
Getting Started
The host
First of all you need a server and storage. This could be an old laptop, a dedicated server, or from cloud, like Hetzner. Though I have to warn you that sharing illegally obtained content on cloud providers like hetzner are against their TOS; however, unlike torrents, in soulseek there is no way for hetzner to tell you you are sharing illegal content. I will assume that you have a working Debian installation in a machine.
Docker & Docker Compose
If you don’t know what docker is at all I recommend you to check it out first, from resources like official 101 tutorial or some youtube videos. But tldr is that docker lets you run applications in a pre-defined blueprint “vm” that is able to utilize all of the hardware of the host machine. And compose lets you literally compose multiple containers in single project and you can bring them up and down with one command.
Install docker on the machine:
curl -fsSL https://get.docker.com -o install-docker.sh
# verify script's content first
# less install-docker.sh
sudo sh install-docker.sh
You can then proceed the post-install instructions in order to run docker as non-root user.
sudo groupadd docker
sudo usermod -aG docker $USER
Restart your shell and now you should be able to run docker without sudo. Test it with docker run hello-world.
Setting up the services
Let’s make a directory selfhosted that will contain all of our compose projects. In the future you can make this a git repo and set up GitOps with Renovate that will update the compose image versions. You can check out a fantastic blog post on how to do that.
mkdir selfhosted
cd selfhosted
Reverse proxy
First things first, let’s set up Traefik. I assume that you want to access you music outside the home network, so I suggest you to get a domain name. Traefik can handle HTTPS certifications with let’s encrypt automatically for you. If you’re like me you can get a cheap .xyz domain name with random numbers for less than a dollar, I got this domain (0007823.xyz) from cloudflare for $0.85. You also need to set up DNS records as well so that your domain points to your server’s IP. It becomes a bit more complicated if you want to expose your home network, however I trust you that you will figure it out. If you need directions check out dynamic dns or tailscale
Make a directory called traefik and create a compose.yaml, and traefik’s static configuration file.
mkdir traefik
cd traefik
touch compose.yaml .env
mkdir static
touch static/traefik.yaml
Here is my configuration:
# traefik/static/traefik.yaml
global:
checkNewVersion: false
sendAnonymousUsage: false
entryPoints:
web:
address: ":80"
http:
redirections:
entryPoint:
to: websecure
scheme: https
permanent: true
websecure:
address: ":443"
http:
tls: true
providers:
docker:
exposedByDefault: false
network: proxy
api:
dashboard: true
insecure: false
log:
level: INFO
certificatesResolvers:
cloudflare:
acme:
email: <your email>
storage: /var/traefik/certs/cloudflare-acme.json
dnsChallenge:
provider: cloudflare
resolvers:
- "1.1.1.1:53"
- "8.8.8.8:53"
And traefik compose:
# traefik/compose.yaml
services:
traefik:
image: traefik:v3.5
container_name: traefik
restart: unless-stopped
networks:
- proxy
ports:
- "80:80"
- "443:443"
environment:
- CF_DNS_API_TOKEN=${CF_DNS_API_TOKEN}
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./static/traefik.yml:/etc/traefik/traefik.yml:ro
- ./data/certs/:/var/traefik/certs/:rw
labels:
- "traefik.enable=true"
- "traefik.http.routers.dashboard.rule=Host(`traefik.<your domain>`)"
- "traefik.http.routers.dashboard.entrypoints=websecure"
- "traefik.http.routers.dashboard.service=api@internal"
- "traefik.http.routers.dashboard.tls=true"
- "traefik.http.routers.dashboard.tls.certresolver=cloudflare"
- "traefik.http.routers.dashboard.middlewares=auth"
- "traefik.http.middlewares.auth.basicauth.users=<user>:<pw hash>"
networks:
proxy:
external: true
# .env file
CF_DNS_API_TOKEN=<ur cloudflare token>
Now, here what we’re doing is using Cloudflare as a DNS Challenge provider for https certificate. You can view traefik’s documentation on acme certificate resolvers and check out all the DNS Providers available for your provider’s required environment variable. Additionally, we’re also exposing traefik dashboard on traefik.your_domain with a basic authentication middleware. In order to set <user>:<pw hash> you can use htpasswd from apache2-utils package
htpasswd -n <username> "<password>" | sed -e 's/\$/\$\$/g'
Double dollars
We need to replace
$with$$, because yaml (or docker idk, and honestly don’t care).
We also set up the docker provider, where containers with configuration labels will automatically be picked up by traefik.
Now if you run docker compose up -d from the traefik directory docker should pull the images and bring up the containers. If you now go to the traefik.your_domain and login with your username and password you should see the Traefik dashboard:

Subsonic server
Let’s now set up gonic. Gonic is very lightweight and perfect for our use.
cd ..
mkdir gonic
cd gonic
touch compose.yaml
Here is my gonic compose file:
# gonic/compose.yaml
services:
gonic:
image: sentriz/gonic:v0.19.0
environment:
- TZ=Asia/Baku
volumes:
- /mnt/music-store/music:/music:ro
- /mnt/music-store/podcasts:/podcasts
- /mnt/music-store/playlists:/playlists
- /var/cache/gonic/cache:/cache
- /var/lib/gonic/data:/data
networks:
- proxy
labels:
- "traefik.enable=true"
- "traefik.http.routers.gonic.rule=Host(`music.<your domain>`)"
- "traefik.http.routers.gonic.entrypoints=websecure"
- "traefik.http.routers.gonic.tls=true"
- "traefik.http.routers.gonic.tls.certresolver=cloudflare"
- "traefik.http.services.gonic.loadbalancer.server.port=80"
networks:
proxy:
external: true
I have mounted my music drive on to the /mnt/music-store, you should change it for your use case. You should set the certresolver key to whatever you set up in the traefik’s certificateResolvers configuration key, for me the name of the key was cloudflare. You can check out configuration options of gonic and set anything additional if you need to, for example you could change transcode cache size or fine tune multi value tags.
You can now run docker compose up -d and should be able to access music.your_domain in the browser. Go ahead and login with admin/admin username and password. It is strongly advised to least change the password after logging in to something other than default. I also suggest you to hook gonic up to your last.fm account here so that you get rich artist info and images.

File sharing network
Now it’s slskd’s turn.
cd ..
mkdir slskd
cd slskd
touch compose.yaml .env
My slskd compose
# slskd/compose.yaml
services:
slskd:
image: ghcr.io/slskd/slskd:0.23.2
container_name: slskd
ports:
- "50300:50300"
environment:
- SLSKD_REMOTE_CONFIGURATION=false
- SLSKD_SHARED_DIR=/music-store/music/
- SLSKD_DOWNLOADS_DIR=/music-store/slskd_downloads
- SLSKD_INCOMPLETE_DIR=/music-store/slskd_incomplete
- SLSKD_USERNAME=${SLSKD_USERNAME}
- SLSKD_PASSWORD=${SLSKD_PASSWORD}
- SLSKD_SLSK_USERNAME=urusername
- SLSKD_SLSK_PASSWORD=urpw
volumes:
- slskd-data:/app
- /mnt/music-store/:/music-store
restart: always
networks:
- proxy
labels:
- "traefik.enable=true"
- "traefik.http.routers.slskd.rule=Host(`slskd.<your domain>`)"
- "traefik.http.routers.slskd.entrypoints=websecure"
- "traefik.http.routers.slskd.tls=true"
- "traefik.http.routers.slskd.tls.certresolver=cloudflare"
- "traefik.http.services.slskd.loadbalancer.server.port=5030"
networks:
proxy:
external: true
volumes:
slskd-data:
and set up web interface username/password in the .env file
SLSKD_USERNAME=uruser
SLSKD_PASSWORD=strongpw
Note that the SLSKD_SLSK_* variables are for connecting to Soulseek network, as far as I know, and it doesn’t really matter what they are as long as they are the same thing after each restart. We need to expose port 50300 becuase that is the port that we will be communicating with other users with. Now do a docker compose up -d and you should be able to search and download songs into your drive.

Tagging and automation
We have come to the final part. Most people for their automation and music organizing use a very popular program called beets, I used to use it to, it is a very mature and feature baked-in program. However, I switched off to wrtag by the author of gonic server for the reasons of speed and simplicity. It is a fantastic piece of software.
Since I will be sharing a secret between wrtag and slskd I decided to put services in the same compose file, you can choose not to, it is up to you. Here is a diff of the compose file and additional config files that I needed
--- compose_slskd.yml
+++ compose.yml
services:
slskd:
image: ghcr.io/slskd/slskd:0.23.2
container_name: slskd
ports:
- "50300:50300"
environment:
+ - SLSKD_CONFIG=/config.yaml
- SLSKD_REMOTE_CONFIGURATION=false
- SLSKD_SHARED_DIR=/music-store/music/
- SLSKD_DOWNLOADS_DIR=/music-store/slskd_downloads
- SLSKD_INCOMPLETE_DIR=/music-store/slskd_incomplete
- SLSKD_USERNAME=${SLSKD_USERNAME}
- SLSKD_PASSWORD=${SLSKD_PASSWORD}
- SLSKD_SLSK_USERNAME=urusername
- SLSKD_SLSK_PASSWORD=urpw
+ - WRTAG_API_KEY=${WRTAG_API_KEY}
+ - WRTAG_URL=wrtag.0007823.xyz
volumes:
- slskd-data:/app
- /mnt/music-store/:/music-store
+ - ./config.yaml:/config.yaml
+ - ./autotag.sh:/autotag.sh
restart: always
networks:
- proxy
labels:
- "traefik.enable=true"
- "traefik.http.routers.slskd.rule=Host(`slskd.<your domain>`)"
- "traefik.http.routers.slskd.entrypoints=websecure"
- "traefik.http.routers.slskd.tls=true"
- "traefik.http.routers.slskd.tls.certresolver=cloudflare"
- "traefik.http.services.slskd.loadbalancer.server.port=5030"
+
+ wrtag:
+ image: ghcr.io/sentriz/wrtag:v0.19.0
+ environment:
+ - WRTAG_WEB_API_KEY=${WRTAG_API_KEY}
+ - WRTAG_WEB_LISTEN_ADDR=:80
+ - WRTAG_WEB_PUBLIC_URL=https://wrtag.your_domain
+ - WRTAG_WEB_DB_PATH=/data/wrtag.db
+ - WRTAG_LOG_LEVEL=info
+ - WRTAG_CONFIG_PATH=/config
+ volumes:
+ - wrtag-data:/data
+ - /mnt/music-store:/music-store
+ - ./wrtag_config:/config
+ networks:
+ - proxy
+ labels:
+ - "traefik.enable=true"
+ - "traefik.http.routers.wrtag.rule=Host(`wrtag.your_domain`)"
+ - "traefik.http.routers.wrtag.entrypoints=websecure"
+ - "traefik.http.routers.wrtag.tls=true"
+ - "traefik.http.routers.wrtag.tls.certresolver=cloudflare"
+ - "traefik.http.services.wrtag.loadbalancer.server.port=80"
+
networks:
proxy:
external: true
volumes:
slskd-data:
+ wrtag-data:
Add additional API Key to env
WRTAG_API_KEY=secureapikeyy
Set up slskd config so that it runs autotag.sh script every time a download completes:
touch config.yaml autotag.sh
# slskd/config.yaml
integration:
scripts:
write_tags:
on:
- DownloadDirectoryComplete
run:
command: "/autotag.sh"
#!/usr/bin/bash
# slskd/autotag.sh
# from: https://github.com/stevewm/homelab/blob/ea6407980259a690b41ead44c45dfff4d398fcba/kubernetes/apps/downloads/slskd/app/config/wrtag.sh
# LICENSE: Unlicense
INPUT_PATH=$(echo "${SLSKD_SCRIPT_DATA}" | jq -r .localDirectoryName)
echo "Starting import of folder from: ${INPUT_PATH}"
# wget is the only thing available in the slskd container
wget -q -O/dev/null --post-data "path=${INPUT_PATH}" "https://'':${WRTAG_API_KEY}@${WRTAG_URL}/op/move"
And now, we set up the wrtagweb to your preference in wrtag_config file. I have setted it up to mine where path-format is recommended, and I have added the addons I use. You can find more information in the readme. Note that currently there’s an issue about genius lyrics provider, so I have excluded it from my config.
# slskd/wrtag_config
path-format /music-store/music/{{ artists .Release.Artists | sort | join "; " | safepath }}/({{ .Release.ReleaseGroup.FirstReleaseDate.Year }}) {{ .Release.Title | safepath }}{{ if not (eq .ReleaseDisambiguation "") }} ({{ .ReleaseDisambiguation | safepath }}){{ end }}/{{ pad0 2 .TrackNum }}.{{ len .Tracks | pad0 2 }} {{ if .IsCompilation }}{{ artistsString .Track.Artists | safepath }} - {{ end }}{{ .Track.Title | safepath }}{{ .Ext }}
addon lyrics lrclib musixmatch
addon replaygain true-peak
log-level info
cover-upgrade
Now whenever we download a directory from slskd and it completes it will tell wrtagweb to move the directory contents from slskd_downloads to the directory specified in the path-format. We have to do one final thing, which is wrtagweb should tell our music server, gonic, that move has completed and it should sync its database. Since the configuration will have password in it I prefer to keep it inside .env file, that way I can ignore in git
WRTAG_NOTIFICATION_URI="complete\,sync-complete generic+https://music.<your_domain>/rest/startScan.view?c=wrtag&v=1.16&u=<admin_uname>&p=<admin_pw>"
where, music.<your_domain> was the domain name that you setted up in Subsonic server and <admin_uname> and <admin_pw> are self-explanatory, I think. You can also stack the notification uris by appending a comma and adding the uri into the environment variable (See notifications)
The only thing to do is docker compose up -d, sip a coffee, install clients, login to the server and enjoy your music.
