Running Velvet with Docker

August 11, 2026 · View on GitHub

⚠️ Upgrading from earlier fork versions — action required

Velvet now runs as the unprivileged node user by default. Before restarting your container after pulling the new image you must:

1. Find your UID/GID (the user that should own the files Velvet writes):

id <your-username>
# example output: uid=1000(jan) gid=1000(jan)

2. Fix ownership of the Velvet data directories on your host (they were written as root before):

# Replace 1000:1000 with your actual UID:GID
chown -R 1000:1000 /path/to/save \
                   /path/to/image-cache \
                   /path/to/waveform-cache

Do not chown your music library — those files are already owned correctly.

3. Set PUID/PGID in your Compose environment: block to the UID/GID from step 1. The entrypoint will repair ownership and then run as that user.

environment:
  PUID: 1000
  PGID: 1000

4. Then pull and restart:

docker compose pull
docker compose down
docker compose up -d

With PUID/PGID set, the entrypoint handles ownership repair automatically — no pre-chown step is needed if you're already on the correct UID/GID.


Migrating from the previous image

The project was previously hosted at github.com/aroundmyroom/mstream (that repository no longer exists). The Docker image has moved:

BeforeAfter
ghcr.io/aroundmyroom/mstream-velvet:vX.Y.Z-velvetghcr.io/aroundmyroom/velvet:vX.Y.Z

or use ghcr.io/aroundmyroom/velvet:latest

Your data is fully compatible. The volume structure (save/, image-cache/, waveform-cache/) is unchanged — no data migration is needed.

Steps

  1. Stop your current container:

    docker compose down
    
  2. Update the image: line in your compose.yaml:

    # Before
    image: ghcr.io/aroundmyroom/mstream-velvet:latest
    
    # After
    image: ghcr.io/aroundmyroom/velvet:latest
    
  3. Pull and restart:

    docker compose pull
    docker compose up -d
    

That's it — all your music, playlists, users, and settings are preserved in the mounted volumes.


Updating to the latest release

If you installed via compose.yaml with image: ghcr.io/aroundmyroom/velvet:latest:

docker compose pull          # fetch the new image
docker compose down
docker compose up -d         # recreate the container

That's it — your save/ folder (config, database, logs) and music volume are mounted from the host, so no data is lost.

Pinned to a specific version? Update the tag in compose.yaml (e.g. v0.0.1), then run the same three commands. Check the GitHub releases page for the latest tag.


Quick start — pull from GitHub Container Registry

The easiest way. No build step required.

docker pull ghcr.io/aroundmyroom/velvet:latest

Or pin to a specific release:

docker pull ghcr.io/aroundmyroom/velvet:v0.3.22
services:
  velvet:
    image: ghcr.io/aroundmyroom/velvet:latest
    container_name: velvet
    restart: unless-stopped
    ports:
      - "3000:3000"
    volumes:
      - ./save:/app/save
      - /media/music:/music         # adjust host path to your library
      - ./waveform-cache:/app/waveform-cache
      - ./image-cache:/app/image-cache
    environment:
      PUID: 1000                   # uid of host user owning the mounted folders (default: 1000)
      PGID: 1000                   # gid of that user (default: 1000)
      VELVET_MUSIC_DIR: /music     # triggers first-run auto-config (optional, see below)
docker compose up -d

Open http://localhost:3000


Build from source

git clone https://github.com/aroundmyroom/Velvet.git
cd Velvet
docker build -t velvet .

Then change the image: line in compose.yaml to velvet.


How the image is published

Every time a v* tag is pushed to GitHub, the workflow .github/workflows/docker-publish.yml automatically:

  1. Builds a multi-arch image (linux/amd64 + linux/arm64)
  2. Pushes it to ghcr.io/aroundmyroom/velvet with the version tag and latest

No manual steps are needed — tagging a release is enough.


Volumes explained

VolumeWhat it storesRequired?
/app/saveConfig file (save/conf/default.json), SQLite database (save/db/velvet.sqlite), logs, sync stateYes — without this, all data is lost on container restart
/music (or any host path)Your music files — must be added to the config as a folder (see below)Yes, unless music is already inside the image
/app/waveform-cachePre-computed waveforms (regenerated if missing, but takes time)Recommended
/app/image-cacheCached album art, podcast art, radio logosRecommended

User / permission mapping

The container defaults to a root entrypoint that repairs host volume ownership and then drops to the unprivileged node user.

Set PUID and PGID in your compose.yaml to the uid/gid that owns your bind-mounted folders. The entrypoint reassigns the internal node user to those ids before dropping privileges, so the process reads and writes files as the correct host user.

Find your uid/gid on the host (or in the NAS shell):

id <your-music-user>
# example output:  uid=1000(soulseek) gid=1000(soulseek)

Then add to your compose.yaml:

    environment:
      PUID: 1000   # uid of the host user who owns the mounted folders
      PGID: 1000   # gid of that user

Both default to 1000 if omitted.

Migration from a root container

If you previously ran as root, the save/, image-cache/, and waveform-cache/ directories on your host may be owned by root. Fix them before restarting:

docker compose down

# Replace 1000:1000 with your actual UID:GID
chown -R 1000:1000 /path/to/save \
                   /path/to/image-cache \
                   /path/to/waveform-cache

docker compose up -d

Your music files are already owned by your NAS user — do not chown those.


First run — adding your music library

On first start Velvet creates a blank config at save/conf/default.json.

Option 1 — environment variables (simple single-library setup)

Add an environment: block to your compose.yaml. Velvet will write the initial config automatically on the very first start and skip this step on every subsequent restart.

Complete copy-paste example:

services:
  velvet:
    image: ghcr.io/aroundmyroom/velvet:latest
    container_name: velvet
    restart: unless-stopped
    ports:
      - "3000:3000"
    volumes:
      - ./save:/app/save            # config, database, logs
      - /media/music:/music         # your music library (adjust host path)
      - ./waveform-cache:/app/waveform-cache
      - ./image-cache:/app/image-cache
    environment:
      PUID: 1000                   # uid of host user owning the mounted folders (default: 1000)
      PGID: 1000                   # gid of that user (default: 1000)
      VELVET_MUSIC_DIR: /music     # must match the volume target above

      # Admin account (optional).
      # If omitted the server starts in open mode — no login required.
      # VELVET_ADMIN_USER: admin
      # VELVET_ADMIN_PASS: changeme

      # Extra feature folders — uncomment to enable.
      # By default each type is applied directly to VELVET_MUSIC_DIR (/music).
      # If your files live in a sub-folder, add the matching *_SUBDIR variable:
      #   VELVET_ENABLE_YOUTUBE: "true"
      #   VELVET_YOUTUBE_SUBDIR: YouTube        # → folder root becomes /music/YouTube
      # You can also add, change or remove folders at any time in the Admin panel.
      # For full control, skip env vars and edit save/conf/default.json directly.

      # AudioBooks & Podcasts  (type: audio-books)
      # VELVET_ENABLE_AUDIOBOOKS: "true"
      # VELVET_AUDIOBOOKS_SUBDIR: Audiobooks    # optional — omit to use /music directly

      # Radio Recordings  (type: recordings — also enables the radio feature)
      # VELVET_ENABLE_RECORDINGS: "true"
      # VELVET_RECORDINGS_SUBDIR: Recordings    # optional — omit to use /music directly

      # YouTube Downloads  (type: youtube)
      # VELVET_ENABLE_YOUTUBE: "true"
      # VELVET_YOUTUBE_SUBDIR: YouTube          # optional — omit to use /music directly
docker compose up -d

Open http://localhost:3000 (or the admin panel at /admin to start a scan).

When env vars are NOT sufficient — use Option 2 instead if you need: multiple mount points, child-vpaths, albumsOnly/filepathPrefix filtering, or any advanced folder layout.

Option 2 — edit the config file directly

Edit save/conf/default.json to point at your music volume:

{
  "folders": {
    "music": {
      "root": "/music"
    }
  }
}

Then restart the container:

docker compose restart

Open the admin panel at http://localhost:3000/admin — no login is required on a fresh install with no users. Start a scan from the Scan button.


Adding users

Once the library has been scanned, create your first user in the admin panel under Users. The first user should have admin access.

After creating at least one user, the server requires login and the no-auth bypass is disabled.


Updating

Pull the latest changes, rebuild the image, and restart:

git pull
docker build -t velvet .
docker compose up -d

Your data in the mounted volumes is untouched.


Useful commands

CommandEffect
docker compose up -dStart in background
docker compose downStop and remove container
docker compose restartRestart after config change
docker compose logs -fFollow live logs
docker exec -it velvet shShell into the running container

Running without Docker Compose

docker run -d \
  --name velvet \
  --restart unless-stopped \
  -p 3000:3000 \
  -v /home/Velvet/save:/app/save \
  -v /media/music:/music \
  -v /home/Velvet/waveform-cache:/app/waveform-cache \
  -v /home/Velvet/image-cache:/app/image-cache \
  velvet

Behind a reverse proxy

If you run Velvet behind nginx or Caddy, see deploy.md for the recommended nginx configuration — required for large FLAC libraries to avoid stall on idle connections.