Skip to content

slskd

Headless Soulseek client with a Web UI + REST API. Acts as both indexer (search) and download client for Lidarr via the Tubifarry plugin. Routes all Soulseek traffic through its own dedicated gluetun container (gluetun-slskd, ProtonVPN WireGuard) so it gets a NAT-PMP forwarded port independent of qBittorrent's — see VPN integration below.

Web UI: http://192.168.1.252:5030/ (LAN-only, via gluetun-slskd's port mapping)

Phase 6D complete — 2026-05-16

slskd 0.25 live on arrstack VM 101 via network_mode: "service:gluetun". Native VPN integration fetching ProtonVPN's NAT-PMP forwarded port from gluetun's control API (X-API-Key auth). Sharing the existing 1,905-dir / 15,986-file music library (read-only mount) + the slskd downloads dir. 5 upload slots, 1 MB/s cap, polite profile blurb. Citizenship-aligned per Soulseek community norms.

Followed by dual-gluetun split — 2026-05-22

slskd moved to its own dedicated gluetun container (gluetun-slskd) after a forwarded-port collision with qBittorrent silently broke qBit's incoming sockets. Network mode and depends-on switched from service:gluetunservice:gluetun-slskd. Lessons in the music-acquisition-bringup runbook.

Service details

Property Value
Host arrstack VM 101 (192.168.1.252) — see Arrstack
Image slskd/slskd:latest
Container name slskd
Network service:gluetun-slskd (dedicated VPN namespace — Soulseek traffic transits its own ProtonVPN session, separate from qBittorrent's)
Web port 5030 (LAN, via gluetun-slskd's published port)
Soulseek listen port Dynamic, fetched from gluetun-slskd's /v1/portforward (rotates with ProtonVPN server changes)
Config / state /opt/mediaserver/slskd/app
Downloads /stash/rodneystash/Downloads/music/downloads
Incomplete /stash/rodneystash/Downloads/music-incomplete/incomplete
Shared (read-only) /stash/rodneystash/Music/music:ro
Soulseek username as set in SLSKD_SLSK_USERNAME env (claimed on first-connect; recycles after 30 days idle)
Admin auth SLSKD_USERNAME / SLSKD_PASSWORD (overrides default slskd/slskd)
Programmatic auth SLSKD_API_KEY (used by Tubifarry)

VPN integration

slskd 0.25 ships native gluetun integration — no sidecar script needed. Five env vars wire it up:

Env var Value Purpose
SLSKD_VPN true Enable VPN-aware mode
SLSKD_VPN_GLUETUN_URL http://localhost:8000 gluetun-slskd's control server (reachable inside the shared namespace)
SLSKD_VPN_GLUETUN_API_KEY ${GLUETUN_CONTROL_SERVER_API_KEY} Authenticates against gluetun-slskd's role-based ACL (config TOML at /opt/mediaserver/gluetun-slskd/auth/config.toml — a copy of the original gluetun's, so the same key works)
SLSKD_VPN_PORT_FORWARDING true slskd waits for gluetun to provide a port before listening
SLSKD_VPN_GLUETUN_TIMEOUT 5000 HttpClient timeout in ms — default 1000 was too aggressive, caused flap-disconnect cycle every ~3 min under any gluetun load

slskd polls gluetun-slskd's /v1/portforward endpoint, gets the dynamic NAT-PMP port (e.g. 50211, rotates with ProtonVPN server changes), binds it on the VPN's tun0 interface for inbound Soulseek peer connections.

Why a dedicated gluetun-slskd (not the shared gluetun)

ProtonVPN's NAT-PMP forwards one port per VPN session. With qBittorrent and slskd sharing one gluetun namespace, both apps wanted the single forwarded port for incoming peers — whichever bound first won, the other silently fell back to a loopback-only socket. The qBit symptom was the kicker: TCP listener succeeded on the wildcard interface (because qBit raced libtorrent's UDP bind), then every tracker showed "Unreachable / skipping tracker announce (unreachable)" in the WebUI because UDP responses had nowhere to land — see the 2026-05-22 follow-up for the diagnostic walk.

Multi-port forwarding on a single ProtonVPN session is tracked in gluetun #2381 — open since 2024-07. Until that lands, the documented community workaround is one gluetun per P2P app. ProtonVPN Plus allows multiple simultaneous WireGuard configs, so this is essentially free in terms of provider cost.

The gluetun-slskd service in arrstack/docker-compose.yml mirrors the original gluetun's environment block but with a separate PROTONVPN_WIREGUARD_PRIVATE_KEY_SLSKD env (generated at account.protonvpn.com → Downloads → WireGuard configuration with NAT-PMP enabled, P2P server). State dir at /opt/mediaserver/gluetun-slskd/; auth config.toml is a copy of the original so the same GLUETUN_CONTROL_SERVER_API_KEY works.

ProtonVPN WireGuard configs expire after one year

Both PROTONVPN_WIREGUARD_PRIVATE_KEY and PROTONVPN_WIREGUARD_PRIVATE_KEY_SLSKD need re-issuing from the ProtonVPN dashboard annually or the tunnels will silently fail.

Config-as-env-vars, not slskd.yml

slskd 0.25's WebUI YAML editor applies edits as Run-Time Overlay (volatile, lost on restart) per the documented precedence chain Defaults < Env Vars < YAML File < CLI Args < Run-Time Overlay. Despite SLSKD_REMOTE_CONFIGURATION=true, "Save" in the YAML editor does NOT persist to disk. The on-disk slskd.yml is the all-commented example file.

All persistent config lives in env vars (defined in the arrstack compose, secrets passed via Dockhand's env UI):

Env var Value Source
SLSKD_USERNAME (admin username) Dockhand env
SLSKD_PASSWORD (admin password) Dockhand env
SLSKD_SLSK_USERNAME (Soulseek username) Dockhand env
SLSKD_SLSK_PASSWORD (Soulseek password) Dockhand env
SLSKD_API_KEY (32-byte hex) Dockhand env — used by Tubifarry
SLSKD_SLSK_DESCRIPTION (profile blurb) Compose (non-secret)
SLSKD_SHARED_DIR /downloads;/music Compose (semicolon-separated list)
SLSKD_UPLOAD_SLOTS 5 Compose
SLSKD_UPLOAD_SPEED_LIMIT 1048576 (1 MB/s) Compose
SLSKD_DOWNLOADS_DIR /downloads Compose — override default /app/downloads to avoid VM rootfs overflow
SLSKD_INCOMPLETE_DIR /incomplete Compose — same

Citizenship policy

Soulseek is a community of humans, not a faceless protocol. Settings reflect 2026 community norms:

  • Share the real library + downloads/music (read-only) + /downloads (read-write). Downloads-only sharing is flagged as a leech anti-pattern; many peers auto-ban small or empty shares.
  • 5 upload slots, 1 MB/s cap — community median is ~1 MB/s in 2026. Conservative slot count keeps the queue moving fairly.
  • Profile blurb signals automationAutomated music library via Lidarr+Tubifarry. Sharing the full collection back as it grows. 5 slots, ~1MB/s up. Won't browse-and-queue your shares. Thanks for sharing! — peers reading user info before allowing uploads can decide.
  • Per-peer throttling enforced in Tubifarry (not slskd) — see Lidarr's slskd indexer config.

Known quirks

  • Failed to start listening on 0.0.0.0:<port> log error is cosmetic when slskd has already bound the same port on specific interfaces. Verify via docker exec gluetun ss -tlnp | grep <port> — multiple per-interface LISTEN entries means inbound traffic on tun0 reaches slskd. The catchall 0.0.0.0 bind just lost a race; doesn't block inbound peers.
  • Path mangling preserves the leaf album folder, strips the peer's parent path. Peer's music\Gotye\Making Mirrors\01-01.flac lands as /downloads/Making Mirrors/01-01.flac. Works correctly for Lidarr's tag-based import.
  • UTF-8 mojibake from peers (e.g., Donât Worry for Don't Worry) persists through the chain. Neither slskd nor Lidarr corrects this on import — fix-on-demand via beets.
  • Username recycling — if slskd is offline >30 days, the Soulseek server reclaims the username (unless you've donated for "privileges"). Non-issue for 24/7 homelab.
  • Lidarr — the consumer; Tubifarry plugin wires slskd as indexer + download client
  • Arrstack — host VM and full compose context
  • Music acquisition bringup runbook — first-boot procedure with hold points + the 2026-05-16 lessons (especially the slskd-config-as-env-vars discovery)