Discord report: container refused to start after pulling latest.
Logs showed `mkdir: cannot create directory '/app/Staging':
Permission denied`. `set -e` in entrypoint.sh then aborted the script
and the container restart-looped.
Root cause traced to commit 70e1750 (2026-05-08, image-bloat fix):
the Dockerfile chown was changed from `chown -R /app` to a scoped
chown on specific subdirs to avoid a redundant layer that was
duplicating the entire /app tree. Side effects:
1. `/app` itself went from soulsync:soulsync (via the recursive walk)
to root:root (Docker WORKDIR default — never re-chowned).
2. `/app/Staging` was the only runtime mount-point dir NOT pre-baked
into the image — every other bind-mountable dir (config, logs,
downloads, Transfer, MusicVideos, scripts) was in the Dockerfile's
`mkdir -p` + `chown` list. Staging was left to the entrypoint.
On rootless Docker / Podman where in-container "root" maps to a host
UID, the entrypoint mkdir on `/app/Staging` could fail with EACCES
depending on the bind-mount path's host ownership.
Fix has three parts:
1. **Dockerfile** — added `/app/Staging` to the runtime mkdir +
scoped chown list. Closes the asymmetry with the other bind-
mountable dirs. Image now ships with the directory pre-baked
owned soulsync:soulsync so the entrypoint mkdir is a guaranteed
no-op even when bind-mount perms are weird.
2. **entrypoint.sh mkdir + chown** — both now have `|| true` so any
future bind-mount permission quirk surfaces as a log line, not
a `set -e` crash + restart loop. Previously only the chown had
the `|| true` suffix; mkdir was bare.
3. **entrypoint.sh writability audit** — new loop at the end of
the setup phase runs `gosu soulsync test -w "$dir"` against
every bind-mountable dir. When a dir isn't writable by the
soulsync user, logs a loud warning with the exact host-side
`chown` command needed to fix it. Catches the underlying bind-
mount perm issue that the restart-loop fix would otherwise mask
(container starts but auto-import / downloads write into
unwritable dirs and fail silently). This is the diagnostic that
would have surfaced the root cause without needing the user to
share a container-restart screenshot.
Zero behavior change for users whose containers were already
starting fine. Defensive against the rootless/podman config that
broke after the image-bloat refactor.
Verified shell syntax with `bash -n entrypoint.sh`. Full pytest
2693 passed (no Python touched).
After the first startup the data directories are already owned by the
correct PUID:PGID. Subsequent restarts now stat /app/data and skip the
expensive recursive walk when ownership is already correct, even when
PUID/PGID differ from the image defaults.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The unconditional chown -R on every container start was walking the
entire /app tree (including large music libraries) even when nothing
needed fixing. Now only the directory nodes themselves are chowned at
startup; the recursive walk still runs inside the UID-change branch
where it is actually needed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Closes#367 (reported by JohnBaumb).
The Docker entrypoint ran `pip install -U yt-dlp --quiet --no-cache-dir`
on every container start. Three problems with that:
- Non-deterministic startup: each restart could pick up a different
yt-dlp version, making "works on my machine" debugging harder.
- Network dependency at boot: PyPI being slow/unreachable gated the
app coming up.
- In-place upgrades inside running containers can race with active
yt-dlp invocations and aren't a great pattern.
Picked Option A from the issue: pin to an exact version in
requirements.txt (`yt-dlp==2026.3.17`) and remove the entrypoint
install entirely. yt-dlp comes baked into the image now via the
existing `pip install -r requirements.txt` in the Dockerfile.
Tradeoff: YouTube fixes ship via SoulSync releases now instead of
"next container restart". The pin is documented inline with how to
bump it.
Net change: -3 entrypoint lines, requirements.txt pin tightened,
WHATS_NEW '2.4.1' block opened (entries hidden until version bumps).
553 tests pass.
Stream source:
- New setting in Settings → Downloads: "Stream / Preview Source"
- Options: YouTube (instant, default) or Active Download Source
- YouTube streams require no auth and are instant
- If active source is Soulseek, automatically falls back to YouTube
- Uses direct client search (bypasses orchestrator's download mode)
- Config key: download_source.stream_source
Docker:
- entrypoint.sh now runs pip install -U yt-dlp on every container
start, so Docker users always have the latest yt-dlp without
rebuilding the image
Updated Dockerfile, entrypoint.sh, and Python code to store database files in /app/data instead of /app/database, avoiding conflicts with the Python package. The database path is now configurable via the DATABASE_PATH environment variable, improving flexibility for container deployments.
Implements manual track matching (discovery fix modal) for YouTube, Tidal, and Beatport platforms, allowing users to search and select Spotify tracks for unmatched results. Adds backend endpoints and frontend logic for updating matches, improves conversion of discovery results for sync/download, and updates Dockerfile/entrypoint for dynamic PUID/PGID/UMASK support. Includes a new DOCKER_PERMISSIONS.md guide.