From f249e62ece0f3106d90a7e79e563325f44e453a3 Mon Sep 17 00:00:00 2001 From: Thales Pereira <31625914+thcp@users.noreply.github.com> Date: Fri, 10 Jul 2026 21:31:15 +0100 Subject: [PATCH 1/7] feat(unraid): publish container to GHCR and add Community Applications template - add docker-publish workflow: build build/Dockerfile and push ghcr.io/stemdeckapp/stemdeck on release + manual dispatch (linux/amd64) - add templates/stemdeck.xml: Unraid Docker template (port 8000, /app/jobs + /cache volumes, persistent library default, optional NVIDIA runtime vars) - add ca_profile.xml at repo root for the CA submission scan - document the GHCR image and Unraid install in README The published image keeps the default Linux x86_64 (CUDA) torch wheel, so a single image runs on CPU by default and uses the GPU when started with --runtime=nvidia; _detect_device() auto-selects CUDA. --- .github/workflows/docker-publish.yml | 85 ++++++++++++++++++++++++++++ README.md | 21 +++++++ ca_profile.xml | 12 ++++ templates/stemdeck.xml | 57 +++++++++++++++++++ 4 files changed, 175 insertions(+) create mode 100644 .github/workflows/docker-publish.yml create mode 100644 ca_profile.xml create mode 100644 templates/stemdeck.xml diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml new file mode 100644 index 00000000..23d33e93 --- /dev/null +++ b/.github/workflows/docker-publish.yml @@ -0,0 +1,85 @@ +name: Docker Publish + +# Builds the server container (build/Dockerfile) and pushes it to GHCR so it can +# be pulled by self-hosted deployments and the Unraid Community Applications app +# (ghcr.io/stemdeckapp/stemdeck). The image keeps the default Linux x86_64 torch +# wheel, which is the CUDA build -- so the same image runs on CPU by default and +# uses the GPU automatically when started with `--runtime=nvidia` (see the +# Unraid template in templates/unraid/). + +on: + release: + types: [published] + # Manual build/push without cutting a release. Defaults to a dev version and + # only tags :edge so it never clobbers :latest. + workflow_dispatch: + inputs: + version: + description: "Version for the build (valid PEP 440, e.g. 0.0.0)" + required: false + default: "0.0.0" + +permissions: {} + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + IMAGE: ghcr.io/${{ github.repository_owner }}/stemdeck + +jobs: + build-and-push: + runs-on: ubuntu-latest + timeout-minutes: 60 + permissions: + contents: read + packages: write + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + + # Derive the version fed to the Dockerfile's VERSION build-arg. On a + # release, use the tag with any leading "v" stripped; on manual runs, use + # the provided input. + - name: resolve version + id: ver + run: | + if [ "${{ github.event_name }}" = "release" ]; then + v="${{ github.event.release.tag_name }}" + else + v="${{ github.event.inputs.version }}" + fi + echo "value=${v#v}" >> "$GITHUB_OUTPUT" + + - name: docker metadata (tags/labels) + id: meta + uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0 + with: + images: ${{ env.IMAGE }} + tags: | + type=raw,value=latest,enable=${{ github.event_name == 'release' && !github.event.release.prerelease }} + type=raw,value=${{ steps.ver.outputs.value }},enable=${{ github.event_name == 'release' }} + type=raw,value=edge,enable=${{ github.event_name == 'workflow_dispatch' }} + + - uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 + + - name: log in to GHCR + uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: build and push + uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 + with: + context: . + file: build/Dockerfile + # Unraid is x86_64; arm64 has no CUDA torch and is not a target. + platforms: linux/amd64 + push: true + build-args: | + VERSION=${{ steps.ver.outputs.value }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + provenance: false diff --git a/README.md b/README.md index feb4ab62..8cca4e0c 100644 --- a/README.md +++ b/README.md @@ -247,6 +247,27 @@ docker compose -f build/docker-compose.yml up --build Stems land in `./jobs/` on the host. Demucs weights are cached in a named volume so they don't re-download on rebuild. Note: no GPU passthrough on macOS Docker. +A prebuilt image is published to GHCR on each release: + +```sh +docker run -d --name stemdeck -p 8000:8000 \ + -v /path/to/jobs:/app/jobs \ + -v /path/to/cache:/cache \ + -e STEMDECK_PERSIST_LIBRARY=1 \ + ghcr.io/stemdeckapp/stemdeck:latest +``` + +On a Linux host with an NVIDIA GPU (driver + NVIDIA Container Toolkit installed), add `--runtime=nvidia -e NVIDIA_VISIBLE_DEVICES=all` and StemDeck auto-detects CUDA. The image already bundles CUDA-enabled torch, so no separate CUDA install is needed. + +#### Unraid + +StemDeck is available in Unraid Community Applications: open **Apps**, search "StemDeck", and install. Map the two volumes to persistent appdata paths: + +- `/app/jobs` -> `/mnt/user/appdata/stemdeck/jobs` (library + stems) +- `/cache` -> `/mnt/user/appdata/stemdeck/cache` (model weights) + +The library is persistent by default (`STEMDECK_PERSIST_LIBRARY=1`), so tracks are never auto-deleted. For GPU acceleration, install the **Nvidia Driver** plugin, then set the container's Extra Parameters to `--runtime=nvidia` (the `NVIDIA_VISIBLE_DEVICES` and `NVIDIA_DRIVER_CAPABILITIES` variables are already in the template). CPU-only works with no extra configuration. + #### `run.sh` control script ```sh diff --git a/ca_profile.xml b/ca_profile.xml new file mode 100644 index 00000000..b8ecce83 --- /dev/null +++ b/ca_profile.xml @@ -0,0 +1,12 @@ + + + + StemDeck splits any song into isolated stems (vocals, drums, bass, guitar, + piano, other) and plays them back in a DAW-style multitrack player, for + practice and transcription. This repository maintains the official StemDeck + self-hosted server container. For help, open an issue on GitHub. + + https://raw.githubusercontent.com/stemdeckapp/stemdeck/main/desktop/src-tauri/icons/icon.png + https://github.com/stemdeckapp/stemdeck + https://github.com/stemdeckapp/stemdeck/issues + diff --git a/templates/stemdeck.xml b/templates/stemdeck.xml new file mode 100644 index 00000000..855635a5 --- /dev/null +++ b/templates/stemdeck.xml @@ -0,0 +1,57 @@ + + + StemDeck + ghcr.io/stemdeckapp/stemdeck:latest + https://github.com/stemdeckapp/stemdeck/pkgs/container/stemdeck + bridge + sh + false + https://github.com/stemdeckapp/stemdeck/issues + https://github.com/stemdeckapp/stemdeck + https://raw.githubusercontent.com/stemdeckapp/stemdeck/main/templates/stemdeck.xml + https://raw.githubusercontent.com/stemdeckapp/stemdeck/main/README.md + https://raw.githubusercontent.com/stemdeckapp/stemdeck/main/desktop/src-tauri/icons/icon.png + http://[IP]:[PORT:8000]/ + + + MediaApp:Music MediaApp:Video Tools: + stems demucs vocal remover karaoke isolation multitrack youtube audio separation + Optional: install the Unraid "Nvidia Driver" plugin and set Extra Parameters to --runtime=nvidia for GPU acceleration. Runs on CPU without a GPU. + + StemDeck splits any song into isolated stems (vocals, drums, bass, guitar, + piano, other) and plays them back in a DAW-style multitrack player. Paste a + YouTube URL or upload a file, get separated tracks you can mute, solo, loop, + slow down, and pitch-shift for practice and transcription. Powered by Demucs. + + Data: processed tracks and downloads live under /app/jobs; Demucs model + weights and the torch cache live under /cache. Map both to persistent + appdata paths so nothing re-downloads on update. + + The library is persistent and user-managed on Unraid + (STEMDECK_PERSIST_LIBRARY=1 is set by default); tracks are never auto-deleted. + + GPU (optional): install the Unraid "Nvidia Driver" plugin, then set Extra + Parameters to --runtime=nvidia and keep NVIDIA_VISIBLE_DEVICES / + NVIDIA_DRIVER_CAPABILITIES. StemDeck auto-detects CUDA and uses it, + processing several times faster. Without a GPU it runs on CPU with no extra + configuration. Note: very new Blackwell cards (RTX 50 series) may fall back + to CPU until a newer CUDA build ships. + + Split songs into stems and play them in a DAW-style multitrack player. Self-hosted, GPU-accelerated with the Unraid Nvidia Driver plugin. + + 8000 + + /mnt/user/appdata/stemdeck/jobs + + /mnt/user/appdata/stemdeck/cache + + 1 + + 1200 + + 3 + + all + + all + From 427c167ea3935e14477f9437809ae9c501e85a21 Mon Sep 17 00:00:00 2001 From: Thales Pereira <31625914+thcp@users.noreply.github.com> Date: Fri, 10 Jul 2026 21:38:48 +0100 Subject: [PATCH 2/7] ci(unraid): derive manual-dispatch version from git instead of 0.0.0 Drop the workflow_dispatch version input and compute it with git describe (hatch-vcs style) so manual builds carry a real dev version. Fetch full history + tags on checkout so git describe resolves. --- .github/workflows/docker-publish.yml | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index 23d33e93..89c457bd 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -10,14 +10,10 @@ name: Docker Publish on: release: types: [published] - # Manual build/push without cutting a release. Defaults to a dev version and - # only tags :edge so it never clobbers :latest. + # Manual build/push without cutting a release. The version is derived from git + # (matching the project's hatch-vcs scheme) and the image only tags :edge, so + # it never clobbers :latest. workflow_dispatch: - inputs: - version: - description: "Version for the build (valid PEP 440, e.g. 0.0.0)" - required: false - default: "0.0.0" permissions: {} @@ -37,17 +33,20 @@ jobs: packages: write steps: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + # Full history + tags so git describe can derive a version on manual runs. + fetch-depth: 0 # Derive the version fed to the Dockerfile's VERSION build-arg. On a - # release, use the tag with any leading "v" stripped; on manual runs, use - # the provided input. + # release, use the tag; on manual runs, derive it from git (hatch-vcs + # style, e.g. 0.8.0a6.dev3+g). Leading "v" stripped either way. - name: resolve version id: ver run: | if [ "${{ github.event_name }}" = "release" ]; then v="${{ github.event.release.tag_name }}" else - v="${{ github.event.inputs.version }}" + v="$(git describe --tags --dirty --always)" fi echo "value=${v#v}" >> "$GITHUB_OUTPUT" From 0c8d7ae415af00e0a26dfff7764b25f5139ec57e Mon Sep 17 00:00:00 2001 From: Thales Pereira <31625914+thcp@users.noreply.github.com> Date: Fri, 10 Jul 2026 21:41:28 +0100 Subject: [PATCH 3/7] ci(unraid): publish a rolling :edge image on merge to main Add a push trigger on main so every merge builds and pushes ghcr.io/stemdeckapp/stemdeck:edge. :edge never moves :latest, which stays reserved for stable releases. --- .github/workflows/docker-publish.yml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index 89c457bd..8da1a27c 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -8,11 +8,13 @@ name: Docker Publish # Unraid template in templates/unraid/). on: + # Every merge to main publishes a rolling :edge image. The version is derived + # from git (hatch-vcs style); :edge never clobbers :latest. + push: + branches: [main] release: types: [published] - # Manual build/push without cutting a release. The version is derived from git - # (matching the project's hatch-vcs scheme) and the image only tags :edge, so - # it never clobbers :latest. + # Same as a main push but on demand, from any ref. workflow_dispatch: permissions: {} @@ -58,7 +60,7 @@ jobs: tags: | type=raw,value=latest,enable=${{ github.event_name == 'release' && !github.event.release.prerelease }} type=raw,value=${{ steps.ver.outputs.value }},enable=${{ github.event_name == 'release' }} - type=raw,value=edge,enable=${{ github.event_name == 'workflow_dispatch' }} + type=raw,value=edge,enable=${{ github.event_name == 'push' || github.event_name == 'workflow_dispatch' }} - uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 From 24f986516540a93912cf53d0ecb49de3076541e3 Mon Sep 17 00:00:00 2001 From: Thales Pereira <31625914+thcp@users.noreply.github.com> Date: Fri, 10 Jul 2026 21:42:35 +0100 Subject: [PATCH 4/7] chore(unraid): point template at :edge until a stable release exists --- templates/stemdeck.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/stemdeck.xml b/templates/stemdeck.xml index 855635a5..00574c6a 100644 --- a/templates/stemdeck.xml +++ b/templates/stemdeck.xml @@ -1,7 +1,7 @@ StemDeck - ghcr.io/stemdeckapp/stemdeck:latest + ghcr.io/stemdeckapp/stemdeck:edge https://github.com/stemdeckapp/stemdeck/pkgs/container/stemdeck bridge sh From 215679605afe59963d21d73e6cb8f164f3454f27 Mon Sep 17 00:00:00 2001 From: Thales Pereira <31625914+thcp@users.noreply.github.com> Date: Fri, 10 Jul 2026 21:43:47 +0100 Subject: [PATCH 5/7] docs(unraid): document edge/latest/version image tags and use :edge in the run example --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 8cca4e0c..c368d95f 100644 --- a/README.md +++ b/README.md @@ -247,14 +247,14 @@ docker compose -f build/docker-compose.yml up --build Stems land in `./jobs/` on the host. Demucs weights are cached in a named volume so they don't re-download on rebuild. Note: no GPU passthrough on macOS Docker. -A prebuilt image is published to GHCR on each release: +A prebuilt image is published to GHCR. Tags: `edge` (rolling, rebuilt on every merge to main), `latest` (newest stable release), and `X.Y.Z` (pinned to a release). ```sh docker run -d --name stemdeck -p 8000:8000 \ -v /path/to/jobs:/app/jobs \ -v /path/to/cache:/cache \ -e STEMDECK_PERSIST_LIBRARY=1 \ - ghcr.io/stemdeckapp/stemdeck:latest + ghcr.io/stemdeckapp/stemdeck:edge ``` On a Linux host with an NVIDIA GPU (driver + NVIDIA Container Toolkit installed), add `--runtime=nvidia -e NVIDIA_VISIBLE_DEVICES=all` and StemDeck auto-detects CUDA. The image already bundles CUDA-enabled torch, so no separate CUDA install is needed. From 104ee5a7725067b9d4631d877c7f80283ae65c11 Mon Sep 17 00:00:00 2001 From: Thales Pereira <31625914+thcp@users.noreply.github.com> Date: Fri, 10 Jul 2026 21:47:51 +0100 Subject: [PATCH 6/7] chore: default run.sh PORT to 8000 to match the container/Unraid port --- run.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/run.sh b/run.sh index a4403782..826ab1ed 100755 --- a/run.sh +++ b/run.sh @@ -6,7 +6,7 @@ set -euo pipefail cd "$(dirname "$0")" HOST="${HOST:-0.0.0.0}" -PORT="${PORT:-8080}" +PORT="${PORT:-8000}" RELOAD="${RELOAD:-0}" # Treat the self-hosted server as a persistent, user-managed library (like the # desktop app): opt out of the 24h job TTL sweep so processed tracks are not From e8cfdf6ef1f8572722e53e096510f4a5cf257583 Mon Sep 17 00:00:00 2001 From: Thales Pereira <31625914+thcp@users.noreply.github.com> Date: Fri, 10 Jul 2026 21:52:12 +0100 Subject: [PATCH 7/7] chore: default advertised port to 8000 across backend and desktop Align DEFAULT_PORT (app/core/settings.py) and the desktop launcher's configured_port() fallback (desktop/src-tauri/src/main.rs) from 8080 to 8000 so every path -- container, run.sh, and desktop -- shares one default. Update the settings comment and the port-default test accordingly. --- app/core/settings.py | 4 ++-- desktop/src-tauri/src/main.rs | 2 +- tests/test_network_gate.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/core/settings.py b/app/core/settings.py index 93cdf121..1b7e9f7e 100644 --- a/app/core/settings.py +++ b/app/core/settings.py @@ -30,7 +30,7 @@ _DURATION_MIN, _DURATION_MAX = 60, 1200 # 1 min .. 20 min _HEIGHT_MIN, _HEIGHT_MAX = 144, 2160 _PORT_MIN, _PORT_MAX = 1024, 65535 -DEFAULT_PORT = 8080 +DEFAULT_PORT = 8000 def _default_allow_network() -> bool: @@ -124,7 +124,7 @@ def set_video_max_height(value: int) -> int: # ── port ── # The preferred port the server binds on launch. The desktop launcher reads this -# (default 8080) before spawning the backend; a self-hosted server's --port wins. +# (default 8000) before spawning the backend; a self-hosted server's --port wins. # Changing it needs a restart — the socket is bound at startup. def get_port() -> int: with _LOCK: diff --git a/desktop/src-tauri/src/main.rs b/desktop/src-tauri/src/main.rs index ed2bfae1..bcd2fd30 100644 --- a/desktop/src-tauri/src/main.rs +++ b/desktop/src-tauri/src/main.rs @@ -1857,7 +1857,7 @@ fn free_port() -> Result<(u16, TcpListener), String> { /// The user's preferred port (Settings -> port), read from the backend's /// settings.json before launch. Defaults to 8080. fn configured_port() -> u16 { - const DEFAULT_PORT: u16 = 8080; + const DEFAULT_PORT: u16 = 8000; let Ok(data_dir) = local_data_dir() else { return DEFAULT_PORT; }; diff --git a/tests/test_network_gate.py b/tests/test_network_gate.py index ed6c90e0..033745f2 100644 --- a/tests/test_network_gate.py +++ b/tests/test_network_gate.py @@ -72,7 +72,7 @@ def test_runtime_settings_round_trip_and_clamp(): def test_port_default_and_clamp(): - assert settings_mod.get_port() == 8080 # default + assert settings_mod.get_port() == 8000 # default with TestClient(app) as c: assert c.post("/api/settings", json={"port": 9000}).json()["port"] == 9000 assert settings_mod.set_port(80) == 1024 # floor (privileged ports rejected)