fix(docker): pre-bake /app/Stream so basic-search playback works on rootless Docker

`core/streaming/prepare.py:94-97` creates /app/Stream lazily via
`os.makedirs(stream_folder, exist_ok=True)` on first playback. Under
standard Docker this works because the container's `root` writes /app
without restriction. Under rootless Docker / Podman the in-container
soulsync UID maps to a host UID that can't write to /app, so the
mkdir silently fails and the streaming "Play" flow errors out with
no obvious user-facing cause.

Same root cause + same fix shape as the May 2026 /app/Staging restart-
loop fix — pre-bake the directory at image build time (when the layer
is owned by root), and thread it through every entrypoint.sh spot that
touches the canonical app-dir list.

Not added to VOLUME — /app/Stream is a transient single-file cache
(cleared on every new playback), no persistence value.

Touched lines:

- Dockerfile: mkdir + chown line that pre-bakes runtime dirs.
- entrypoint.sh: the recursive chown gated on UID change, the always-runs
  mkdir + chown, and the writability audit loop.

No code change. Streaming tests pass unchanged (they use tmp_path, not
/app/Stream).
pull/656/head
Broque Thomas 6 days ago
parent 02dc776692
commit a33faaeb38

@ -73,8 +73,14 @@ COPY --chown=soulsync:soulsync --from=webui-builder /app/webui/static/dist /app/
# pre-baked directory would crash the container into a restart loop on
# rootless Docker/Podman where in-container "root" can't write to /app.
# Pre-baking the dir here makes the entrypoint mkdir a guaranteed no-op.
RUN mkdir -p /app/config /app/data /app/logs /app/downloads /app/Transfer /app/Staging /app/MusicVideos /app/scripts && \
chown soulsync:soulsync /app/config /app/data /app/logs /app/downloads /app/Transfer /app/Staging /app/MusicVideos /app/scripts
# NOTE: /app/Stream is the transient single-file streaming cache used by
# the basic-search "Play" flow (cleared per use, never persistent). It's
# created lazily by `core/streaming/prepare.py` via `os.makedirs`, which
# fails silently on rootless Docker where the soulsync UID can't write
# to /app — playback then errors out with no obvious cause. Pre-baking
# at build time (when the layer is owned by root) avoids that path.
RUN mkdir -p /app/config /app/data /app/logs /app/downloads /app/Transfer /app/Staging /app/Stream /app/MusicVideos /app/scripts && \
chown soulsync:soulsync /app/config /app/data /app/logs /app/downloads /app/Transfer /app/Staging /app/Stream /app/MusicVideos /app/scripts
# Create defaults directory and copy template files
# These will be used by entrypoint.sh to initialize empty volumes

@ -40,7 +40,7 @@ if [ "$CURRENT_UID" != "$PUID" ] || [ "$CURRENT_GID" != "$PGID" ]; then
DATA_OWNER=$(stat -c '%u:%g' /app/data 2>/dev/null || echo "unknown")
if [ "$DATA_OWNER" != "$PUID:$PGID" ]; then
echo "🔒 Fixing permissions on app directories..."
chown -R soulsync:soulsync /app/config /app/data /app/logs /app/downloads /app/Transfer /app/Staging 2>/dev/null || true
chown -R soulsync:soulsync /app/config /app/data /app/logs /app/downloads /app/Transfer /app/Staging /app/Stream 2>/dev/null || true
else
echo "✅ App directory permissions already correct"
fi
@ -81,15 +81,15 @@ chown soulsync:soulsync /app/config/settings.py 2>/dev/null || true
# Pre-mid-2026 the chown line had `|| true` but mkdir didn't — combined
# with `set -e`, a permission-denied mkdir crashed the container into a
# restart loop. Both lines are now best-effort.
mkdir -p /app/config /app/data /app/logs /app/downloads /app/Transfer /app/Staging 2>/dev/null || true
chown soulsync:soulsync /app/config /app/data /app/logs /app/downloads /app/Transfer /app/Staging 2>/dev/null || true
mkdir -p /app/config /app/data /app/logs /app/downloads /app/Transfer /app/Staging /app/Stream 2>/dev/null || true
chown soulsync:soulsync /app/config /app/data /app/logs /app/downloads /app/Transfer /app/Staging /app/Stream 2>/dev/null || true
# Writability audit — surface a loud warning if any bind-mounted dir
# isn't writable by the soulsync user. The restart-loop fix above makes
# the container start regardless, but a non-writable Staging / downloads
# / Transfer will fail silently inside the app (auto-import quarantine,
# download writes). Better to log now than to debug missing files later.
for dir in /app/config /app/data /app/logs /app/downloads /app/Transfer /app/Staging /app/MusicVideos /app/scripts; do
for dir in /app/config /app/data /app/logs /app/downloads /app/Transfer /app/Staging /app/Stream /app/MusicVideos /app/scripts; do
if [ -d "$dir" ] && ! gosu soulsync test -w "$dir" 2>/dev/null; then
echo "⚠️ WARNING: $dir is not writable by soulsync (uid $(id -u soulsync))."
echo " Host bind-mount perms likely mismatch the PUID/PGID env vars."

@ -3423,6 +3423,7 @@ const WHATS_NEW = {
{ title: 'Fix: MusicBrainz album clicks 404ing in enhanced search', desc: 'every click on a MusicBrainz album result was silently 404-ing — the /release fetch was passing `cover-art-archive` as an `inc` param, which MB rejects with 400 (that field is returned on every release response by default, no include needed). dropped the bad include; album detail now loads correctly.' },
{ title: 'Fix popup: paste a MusicBrainz URL or MBID to match directly', desc: 'new escape hatch on the Fix Track Match modal (the 🔧 Fix button on mirrored / YouTube / Tidal / Deezer / Beatport / ListenBrainz / Spotify-public discovery rows). when fuzzy search keeps ranking the wrong recording among many same-title versions, paste the MusicBrainz recording URL like `https://musicbrainz.org/recording/<uuid>` or the bare UUID into the new field and hit "Look up". skips all fuzzy logic, resolves straight to that record, and runs it through the same confirm + match pipeline.' },
{ title: 'Fix popup: MusicBrainz added to the auto-search cascade', desc: 'the Fix Track Match modal used to query only Spotify → Deezer → iTunes for the auto-search, leaving MusicBrainz out of the loop entirely — even for users with MusicBrainz set as their primary metadata source. now MB is part of the cascade. when MB is your primary, it gets queried first; otherwise it sits as the last fallback. catches niche / non-mainstream / canonical-with-diacritics recordings that the commercial sources miss. Discogs is intentionally absent — Discogs has no track-level search API.' },
{ title: 'Fix: Docker basic-search streaming silently failed under rootless Docker', desc: 'the streaming "Play" flow on the basic search page tried to create `/app/Stream` lazily at runtime, which fails silently when the container runs under rootless Docker / Podman (in-container root can\'t write to `/app`). pre-baked the directory at image build time, matching the same pattern that fixed `/app/Staging` earlier in the cycle. non-persistent — no volume needed.' },
],
'2.5.5': [
{ date: 'May 17, 2026 — 2.5.5 release' },

Loading…
Cancel
Save