Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 86 additions & 0 deletions .github/workflows/docker-publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
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:
# 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]
# Same as a main push but on demand, from any ref.
workflow_dispatch:

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
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; on manual runs, derive it from git (hatch-vcs
# style, e.g. 0.8.0a6.dev3+g<sha>). 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="$(git describe --tags --dirty --always)"
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 == 'push' || 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
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. 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: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.

#### 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
Expand Down
4 changes: 2 additions & 2 deletions app/core/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down
12 changes: 12 additions & 0 deletions ca_profile.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<CommunityApplications>
<Profile>
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.
</Profile>
<Icon>https://raw.githubusercontent.com/stemdeckapp/stemdeck/main/desktop/src-tauri/icons/icon.png</Icon>
<WebPage>https://github.com/stemdeckapp/stemdeck</WebPage>
<Forum>https://github.com/stemdeckapp/stemdeck/issues</Forum>
</CommunityApplications>
2 changes: 1 addition & 1 deletion desktop/src-tauri/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
Expand Down
2 changes: 1 addition & 1 deletion run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
57 changes: 57 additions & 0 deletions templates/stemdeck.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<?xml version="1.0"?>
<Container version="2">
<Name>StemDeck</Name>
<Repository>ghcr.io/stemdeckapp/stemdeck:edge</Repository>
<Registry>https://github.com/stemdeckapp/stemdeck/pkgs/container/stemdeck</Registry>
<Network>bridge</Network>
<Shell>sh</Shell>
<Privileged>false</Privileged>
<Support>https://github.com/stemdeckapp/stemdeck/issues</Support>
<Project>https://github.com/stemdeckapp/stemdeck</Project>
<TemplateURL>https://raw.githubusercontent.com/stemdeckapp/stemdeck/main/templates/stemdeck.xml</TemplateURL>
<ReadMe>https://raw.githubusercontent.com/stemdeckapp/stemdeck/main/README.md</ReadMe>
<Icon>https://raw.githubusercontent.com/stemdeckapp/stemdeck/main/desktop/src-tauri/icons/icon.png</Icon>
<WebUI>http://[IP]:[PORT:8000]/</WebUI>
<ExtraParams></ExtraParams>
<PostArgs></PostArgs>
<Category>MediaApp:Music MediaApp:Video Tools:</Category>
<ExtraSearchTerms>stems demucs vocal remover karaoke isolation multitrack youtube audio separation</ExtraSearchTerms>
<Requires>Optional: install the Unraid "Nvidia Driver" plugin and set Extra Parameters to --runtime=nvidia for GPU acceleration. Runs on CPU without a GPU.</Requires>
<Overview>
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.
</Overview>
<Description>Split songs into stems and play them in a DAW-style multitrack player. Self-hosted, GPU-accelerated with the Unraid Nvidia Driver plugin.</Description>

<Config Name="WebUI Port" Target="8000" Default="8000" Mode="tcp" Description="HTTP port for the StemDeck web player." Type="Port" Display="always" Required="true" Mask="false">8000</Config>

<Config Name="Jobs / Library" Target="/app/jobs" Default="/mnt/user/appdata/stemdeck/jobs" Mode="rw" Description="Processed tracks, stems, and downloaded audio. Persistent, user-managed library." Type="Path" Display="always" Required="true" Mask="false">/mnt/user/appdata/stemdeck/jobs</Config>

<Config Name="Cache (models)" Target="/cache" Default="/mnt/user/appdata/stemdeck/cache" Mode="rw" Description="Demucs model weights (~170 MB) and the torch cache. Persist so they survive updates." Type="Path" Display="always" Required="true" Mask="false">/mnt/user/appdata/stemdeck/cache</Config>

<Config Name="Persistent library" Target="STEMDECK_PERSIST_LIBRARY" Default="1" Description="Keep 1 so processed tracks are never auto-deleted (recommended on Unraid). Set 0 to re-enable the 24h cleanup sweep." Type="Variable" Display="advanced" Required="false" Mask="false">1</Config>

<Config Name="Max track length (seconds)" Target="STEMDECK_MAX_DURATION_SEC" Default="1200" Description="Reject sources longer than this. Default 1200 (20 min)." Type="Variable" Display="advanced" Required="false" Mask="false">1200</Config>

<Config Name="Max concurrent jobs" Target="STEMDECK_MAX_PENDING_JOBS" Default="3" Description="How many separations may be queued/running at once (1-50)." Type="Variable" Display="advanced" Required="false" Mask="false">3</Config>

<Config Name="NVIDIA GPUs" Target="NVIDIA_VISIBLE_DEVICES" Default="all" Description="GPU(s) exposed to the container. Requires the Unraid Nvidia Driver plugin and Extra Parameters set to --runtime=nvidia. Use 'all' or a specific GPU UUID." Type="Variable" Display="advanced" Required="false" Mask="false">all</Config>

<Config Name="NVIDIA driver capabilities" Target="NVIDIA_DRIVER_CAPABILITIES" Default="all" Description="Driver features exposed to the container. Leave as 'all' for CUDA compute." Type="Variable" Display="advanced" Required="false" Mask="false">all</Config>
</Container>
2 changes: 1 addition & 1 deletion tests/test_network_gate.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down