Running Velvet with Docker
August 11, 2026 · View on GitHub
⚠️ Upgrading from earlier fork versions — action required
Velvet now runs as the unprivileged
nodeuser 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-cacheDo not chown your music library — those files are already owned correctly.
3. Set
PUID/PGIDin your Composeenvironment:block to the UID/GID from step 1. The entrypoint will repair ownership and then run as that user.environment: PUID: 1000 PGID: 10004. Then pull and restart:
docker compose pull docker compose down docker compose up -dWith
PUID/PGIDset, 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:
| Before | After |
|---|---|
ghcr.io/aroundmyroom/mstream-velvet:vX.Y.Z-velvet | ghcr.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
-
Stop your current container:
docker compose down -
Update the
image:line in yourcompose.yaml:# Before image: ghcr.io/aroundmyroom/mstream-velvet:latest # After image: ghcr.io/aroundmyroom/velvet:latest -
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
compose.yaml (ghcr.io — recommended)
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
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:
- Builds a multi-arch image (
linux/amd64+linux/arm64) - Pushes it to
ghcr.io/aroundmyroom/velvetwith the version tag andlatest
No manual steps are needed — tagging a release is enough.
Volumes explained
| Volume | What it stores | Required? |
|---|---|---|
/app/save | Config file (save/conf/default.json), SQLite database (save/db/velvet.sqlite), logs, sync state | Yes — 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-cache | Pre-computed waveforms (regenerated if missing, but takes time) | Recommended |
/app/image-cache | Cached album art, podcast art, radio logos | Recommended |
User / permission mapping
The container defaults to a root entrypoint that repairs host volume ownership and then drops to the unprivileged node user.
Recommended — PUID / PGID environment variables
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/filepathPrefixfiltering, 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
| Command | Effect |
|---|---|
docker compose up -d | Start in background |
docker compose down | Stop and remove container |
docker compose restart | Restart after config change |
docker compose logs -f | Follow live logs |
docker exec -it velvet sh | Shell 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.