Skip to content

fix(docker): the published image crash-loops on a fresh volume; smoke-test that it runs - #9

Merged
topkoa merged 2 commits into
mainfrom
fix/cache-volume-permissions
Jul 12, 2026
Merged

topkoa merged 2 commits into
mainfrom
fix/cache-volume-permissions

Conversation

@topkoa

@topkoa topkoa commented Jul 12, 2026 •

Copy link
Copy Markdown
Contributor

The image we published today cannot start

It builds perfectly. Then, on first run:

PermissionError: [Errno 13] Permission denied: '/app/cache/_roformer-models'
[entrypoint] Server exited and auto-update disabled. Exiting.

…and restart: unless-stopped turns that into a crash-loop. This breaks the documented path too — docker compose up -d straight from the README does exactly this.

Found by actually running it (docker run the published image against a fresh named volume), not by reading it.

Cause

/app/cache doesn't exist in the image — there's no cache/ in the repo and it's .dockerignored. So chown -R appuser /app never touches it.

Docker initializes a fresh named volume from whatever sits at that mountpoint in the image, ownership included. With nothing there, it creates the directory as root:root. The container runs as appuser (uid 10001) and dies on its first write.

Verified, not inferred:

$ docker run --rm -v feedback-demucs-cache:/app/cache alpine ls -ldn /app/cache
drwxr-xr-x  2 0 0 4096 /app/cache          ← root

$ docker run --rm --entrypoint sh ghcr.io/.../feedback-demucs-server:latest -c id
uid=10001(appuser) gid=10001(appuser)      ← not root

Credit to @mbanks850 — PR #5 is titled "Fix build and permission errors in Dockerfile". They hit this. I read that PR as build-only and told them it was worth keeping just for the rename; that was wrong, and I've said so on their PR.

The fix

Create /app/cache and chown it before the VOLUME instruction, so a fresh volume inherits uid 10001.

The real fix: a smoke test

A green build certified an image that cannot run. A build proves the layers assembled; it says nothing about whether the thing starts. So CI now starts the container the way a user does and requires it to serve /health.

Two things about the test are deliberate:

  • It mounts a fresh named volume. This is load-bearing. Without it the container writes to the image's own filesystem, the permission bug never fires, and the test passes on the very bug it exists to catch.
  • It doesn't stop at /health. A 200 there is necessary but not sufficient — the server only touches the cache once a job arrives, so it can answer /health and still be broken. The test also writes a probe file into /app/cache as the runtime user, which is the precise thing that failed.

It also fails fast if the container leaves running, instead of burning two minutes polling something a restart policy is bouncing in a loop.

This PR's own CI run is the proof. If the smoke test passes here, the image genuinely runs.

⚠️ Existing volumes need removing — pulling the fix is not enough

Docker sets a volume's ownership only when it first creates it. A volume made by the broken image stays root-owned forever and will keep crash-looping on a perfectly good image. Anyone who ran the image today must:

docker compose down
docker volume rm feedback-demucs-cache
docker compose up -d

Documented in the README, because without it a fixed image still generates "still broken" reports. The volume only holds cached weights — deleting it costs a re-download and nothing else.

The feedBack stem-splitter plugin detects this exact case (root-owned volume + crash-loop) and tells the user the same thing, since they can't run docker logs from inside the app.

Summary by CodeRabbit

  • Bug Fixes

    • Improved container startup reliability by ensuring the cache directory is created with the correct permissions for the non-root runtime user.
    • Added build-time smoke testing to verify the container starts, responds on /health, and can write to the cache directory.
  • Documentation

    • Updated Docker Compose GPU instructions, including clearer guidance for macOS versus Linux/Windows/WSL2.
    • Added a warning and step-by-step recovery procedure for restoring permissions when using cache volumes created by older images.

…-test that it runs

The image we published an hour ago cannot start. It builds perfectly, then:

    PermissionError: [Errno 13] Permission denied: '/app/cache/_roformer-models'
    [entrypoint] Server exited and auto-update disabled. Exiting.

...and `restart: unless-stopped` turns that into a crash-loop.

Cause. /app/cache does not exist in the image (no cache/ in the repo, and it is
.dockerignore'd), so `chown -R appuser /app` never touches it. Docker initializes a fresh
named volume from whatever sits at the mountpoint in the image - ownership included - and
with nothing there it creates the directory as root:root. The container runs as appuser
(uid 10001) and dies on its first write. Verified, not inferred: the volume's contents are
owned by 0:0, and `id` in the image reports uid=10001.

This breaks the DOCUMENTED path exactly as hard: `docker compose up -d` from the README
does the same thing. Credit to @mbanks850, whose #5 ("Fix build and permission errors in
Dockerfile") hit this - I misread it as build-only and owe them a correction.

Fix: create /app/cache and chown it BEFORE the VOLUME instruction, so a fresh volume
inherits uid 10001.

A green build certified a broken image, so also: a SMOKE TEST in CI. It starts the
container the way a user does and requires it to serve /health. Two things about it are
deliberate:

  * It mounts a FRESH NAMED VOLUME. That is the load-bearing part. Without it the
    container writes to the image's own filesystem, the permission bug never fires, and
    the test passes on the very bug it exists to catch.
  * It does not stop at /health. A 200 there is necessary but not sufficient - the server
    only touches the cache once a job arrives, so it can answer /health and still be
    broken. The test also writes a probe file into /app/cache AS THE RUNTIME USER, which
    is the precise thing that failed.

It fails fast if the container leaves `running`, rather than burning two minutes polling
something a restart policy is bouncing in a loop.

README: pulling the fixed image is NOT enough on its own. Docker sets a volume's ownership
only when it first creates it, so a volume made by the broken image stays root-owned
forever and will keep crash-looping on a perfectly good image. Users must
`docker volume rm feedback-demucs-cache`. Without that note, a fixed image still generates
"still broken" reports.

Signed-off-by: topkoa <topkoa@gmail.com>
@coderabbitai

coderabbitai Bot commented Jul 12, 2026 •

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: c8928e9e-9f76-4788-81fa-729c72e0cc34

📥 Commits

Reviewing files that changed from the base of the PR and between 7eb5f00 and 6a984bd.

📒 Files selected for processing (1)
  • README.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • README.md

📝 Walkthrough

Walkthrough

The image prepares /app/cache for non-root execution, documentation covers GPU configuration and stale cache volumes, and CI smoke-tests container health and cache write access.

Changes

Container runtime validation

Layer / File(s) Summary
Cache ownership and deployment guidance
Dockerfile, README.md
The image creates /app/cache before declaring the volume, while the README updates GPU instructions and documents removal of stale root-owned cache volumes.
Container health and cache smoke test
.github/workflows/docker-build.yml
The workflow starts the local image, polls /health, verifies cache write access as the runtime user, reports failures, and cleans up resources.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant DockerBuildWorkflow
  participant BuiltContainer
  participant HealthEndpoint
  participant CacheVolume
  DockerBuildWorkflow->>BuiltContainer: start image with cache volume
  DockerBuildWorkflow->>HealthEndpoint: poll /health
  HealthEndpoint-->>DockerBuildWorkflow: readiness response
  DockerBuildWorkflow->>CacheVolume: write and delete probe file
  DockerBuildWorkflow->>BuiltContainer: clean up container
  DockerBuildWorkflow->>CacheVolume: remove test volume
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the Docker crash-loop fix and added smoke test.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/cache-volume-permissions

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
.github/workflows/docker-build.yml (2)

97-101: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Consider passing the tag via env var instead of direct template expansion.

zizmor flags "${{ steps.tag.outputs.scan }}" inside run: as a template-injection pattern. Risk here is low (value derives from repo name + commit SHA), but routing it through an env var avoids the anti-pattern generally.

🔒 Proposed fix
       - name: Smoke test — the container must start and serve /health
         run: |
           set -euo pipefail
           docker volume rm -f smoke-cache >/dev/null 2>&1 || true
           docker run -d --name smoke \
             -e SKIP_WARMUP=true \
             -v smoke-cache:/app/cache \
             -p 7865:7865 \
-            "${{ steps.tag.outputs.scan }}"
+            "$SCAN_TAG"
+        env:
+          SCAN_TAG: ${{ steps.tag.outputs.scan }}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/docker-build.yml around lines 97 - 101, Update the docker
run step to pass steps.tag.outputs.scan through an environment variable, then
reference that variable in the command instead of directly expanding the GitHub
Actions expression in run:. Preserve the existing container options and image
tag value.

Source: Linters/SAST tools


122-141: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Cache-write-probe failure exits without cleanup.

Lines 135-136 exit 1 on probe failure, skipping the docker rm/docker volume rm at 139-140 (unlike the earlier /health failure path, which cleans up before exiting). Harmless on ephemeral GH-hosted runners, but the duplicated cleanup logic across two exit paths is easy to consolidate.

♻️ Proposed fix using a trap
       - name: Smoke test — the container must start and serve /health
         run: |
           set -euo pipefail
           docker volume rm -f smoke-cache >/dev/null 2>&1 || true
+          cleanup() {
+            docker rm -f smoke >/dev/null 2>&1 || true
+            docker volume rm -f smoke-cache >/dev/null 2>&1 || true
+          }
+          trap cleanup EXIT
           docker run -d --name smoke \
             ...
           if [ -z "$ok" ]; then
             echo "::error::/health never answered. The image builds but does not run."
-            docker rm -f smoke >/dev/null 2>&1 || true
-            docker volume rm -f smoke-cache >/dev/null 2>&1 || true
             exit 1
           fi
           ...
           docker exec smoke sh -c 'touch /app/cache/.write-probe && rm /app/cache/.write-probe' \
             || { echo "::error::/app/cache is not writable by the runtime user"; exit 1; }
           echo "cache dir is writable by the runtime user"
-
-          docker rm -f smoke >/dev/null 2>&1 || true
-          docker volume rm -f smoke-cache >/dev/null 2>&1 || true
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/docker-build.yml around lines 122 - 141, Consolidate
smoke-container cleanup in the workflow’s smoke-test section so every failure
path, including the cache write probe, removes both the smoke container and
smoke-cache volume before exiting. Use a shell trap or equivalent centralized
cleanup, remove the probe’s direct exit path cleanup duplication, and preserve
the existing failure messages and status.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@README.md`:
- Around line 203-205: Update the fenced code block near the PermissionError
example in README.md to include the text language tag, changing the opening
fence to ```text while preserving the error content unchanged.

---

Nitpick comments:
In @.github/workflows/docker-build.yml:
- Around line 97-101: Update the docker run step to pass steps.tag.outputs.scan
through an environment variable, then reference that variable in the command
instead of directly expanding the GitHub Actions expression in run:. Preserve
the existing container options and image tag value.
- Around line 122-141: Consolidate smoke-container cleanup in the workflow’s
smoke-test section so every failure path, including the cache write probe,
removes both the smoke container and smoke-cache volume before exiting. Use a
shell trap or equivalent centralized cleanup, remove the probe’s direct exit
path cleanup duplication, and preserve the existing failure messages and status.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 8f4ca020-b6bf-492f-ae75-99040159f6e7

📥 Commits

Reviewing files that changed from the base of the PR and between e02e09b and 7eb5f00.

📒 Files selected for processing (3)
  • .github/workflows/docker-build.yml
  • Dockerfile
  • README.md

Comment thread README.md Outdated
The PermissionError sample in the volume-permissions warning had no language on its fence.

Signed-off-by: topkoa <topkoa@gmail.com>
@topkoa
topkoa merged commit 68ce8f0 into main Jul 12, 2026
5 checks passed
@topkoa
topkoa deleted the fix/cache-volume-permissions branch July 12, 2026 17:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant