Repository navigation
Installation
Streamline is a single binary with no external dependencies — no database server, no runtime, no CGO. Pick whichever install method matches how you already run things.
A couple of features are gated behind the ffmpeg/ffprobe binaries, but they are opt-in extras and never a requirement: without them Streamline installs, boots and runs the same, just without media info and import verification. See Optional: ffmpeg.
| Method | Pick this if… |
|---|---|
| Docker Compose | You want the simplest self-hosted setup, with ffmpeg included out of the box |
| Plain binary | You'd rather not run a container, or your hardware doesn't suit one |
| Unraid, Synology, TrueNAS | You run one of these NAS OSes and prefer its own Docker UI |
| Kubernetes / Helm | You already run Kubernetes and want a GitOps-style, declarative deploy |
| NixOS / Nix | You run NixOS, or want the service declared in a flake |
- Before you start: the folder rule
- Docker Compose — recommended for most people
- Plain binary
- Unraid, Synology, TrueNAS
- Kubernetes / Helm
- NixOS / Nix — package, NixOS module and home-manager module
- Optional: ffmpeg
- Verifying what you downloaded
This is the single most common setup mistake, so it goes first. Streamline needs two directories:
- Downloads — where your torrent client puts finished files
- Media — where your organised library lives, the folder Plex/Jellyfin/Emby reads
By default Streamline hardlinks files from downloads into media. A hardlink is a second name for the same data on disk: the file appears in both places but occupies space once, and your torrent client can keep seeding the original untouched.
Important
Hardlinks only work within one filesystem. If downloads and media are on different disks, different volumes, or mounted into the container as two unrelated paths, hardlinking fails and Streamline errors out rather than silently doubling your disk usage. Mount one parent directory, not two children.
Do this:
volumes:
- /srv/data:/data-root # contains both media/ and downloads/Not this:
volumes:
- /srv/data/media:/media # ✗ two separate mounts — different
- /srv/data/downloads:/downloads # ✗ filesystems as far as the container knowsWarning
Your torrent client needs the same layout mounted at the same paths. If they don't match, the paths it reports won't resolve inside Streamline's container, and imports silently break.
If you genuinely can't put them on one filesystem, set library.import_mode to copy (keeps the torrent seeding, uses double the space) or move (saves space, kills seeding). See the Configuration Reference.
The image is self-contained: Streamline plus static ffmpeg and ffprobe binaries in /usr/local/bin, so the optional media features work out of the box with no extra config and no second container.
mkdir -p config data
docker run --rm -v "$PWD/config:/etc/streamline" \
ghcr.io/datahearth/streamline:latest \
config init --output /etc/streamline/config.yamlThis writes a fully-commented config file with every key at its default. You'll edit it in First-Run Setup.
services:
streamline:
image: ghcr.io/datahearth/streamline:latest
container_name: streamline
restart: unless-stopped
ports:
- "8080:8080"
volumes:
- ./data:/data
- ./config:/etc/streamline
# One mount covering both media and downloads — see the folder rule above.
- /srv/data:/srv/dataThen tell Streamline where things live, in config/config.yaml:
library:
movie_path: /srv/data/media/movies
series_path: /srv/data/media/series
download_path: /srv/data/downloadsdocker compose up -d
docker compose logs -f streamlineOpen http://localhost:8080.
Important
Mounting the config read-only? :ro is safe for a GitOps-style deploy, but Streamline writes a few generated values back on first boot (session signing secret, Plex client ID, and a generated admin password if you didn't set one). With a read-only mount you must supply those yourself — see GitOps and Kubernetes.
| Tag | What it tracks |
|---|---|
latest |
Most recent stable release |
vX.Y.Z, X.Y, X
|
Pinned release / minor line / major line |
edge |
Every push to main — expect breakage |
sha-<short> |
An exact commit |
Tip
Pin to vX.Y.Z or at least X.Y for anything you care about.
The transcoder can encode on an Intel or AMD GPU through VAAPI instead of the CPU. The default image cannot do this: its ffmpeg is a static build with no libva, and passing a GPU into it changes nothing. Every release also publishes a second image with a -vaapi tag suffix, built on linuxserver's ffmpeg image (ffmpeg 9, ~1.2 GB), for exactly this case — no Debian, Ubuntu or Alpine ffmpeg package is built with libvmaf, so this is also the only image where transcoding.verify.min_vmaf is actually enforced rather than silently skipped, and it carries av1_vaapi alongside the h264/hevc encoders.
Enabling VAAPI in a container, on Kubernetes, or bare metal
It is not distroless, and it is the image to run if you want hardware encoding in a container.
The container needs the render node and the host group that owns it:
docker run ... \
--device /dev/dri/renderD128 \
--group-add "$(stat -c %g /dev/dri/renderD128)" \
ghcr.io/datahearth/streamline:vX.Y.Z-vaapiOr in compose.yaml:
services:
streamline:
image: ghcr.io/datahearth/streamline:vX.Y.Z-vaapi
devices:
- /dev/dri/renderD128:/dev/dri/renderD128
group_add:
- "989" # the gid of /dev/dri/renderD128 on the host: stat -c %g /dev/dri/renderD128With the Helm chart, set hwAccel.enabled: true and pick the -vaapi image tag. The chart requests the GPU through a device plugin rather than a hostPath or a privileged pod, so the cluster needs one installed: hwAccel.resourceName defaults to gpu.intel.com/i915 and is amd.com/gpu for AMD's plugin. hwAccel.device is the node inside the pod (default /dev/dri/renderD128), and hwAccel.supplementalGroups is the list of video/render gids to grant, which depends on the host distribution.
Then set transcoding.hw_accel (default auto, so usually nothing to do) and transcoding.hw_device if your node is not renderD128. Settings → Transcoding reports whether the device answered. Set it to vaapi instead of auto when the CPU is not an acceptable fallback — on a memory-capped container, say: jobs then wait an hour and re-probe rather than encoding in software. A bare-metal install needs no image at all: any ffmpeg with VAAPI on $PATH or in ffmpeg.path will do. See Configuration Reference for the two keys.
Requires nothing but the binary itself — no runtime, no database server, and no ffmpeg unless you want the optional extras. Grab it from the Releases page — Linux, macOS and Windows, amd64 and arm64.
# Linux amd64 — substitute the real version number
curl -fsSL -o streamline.tar.gz \
https://github.com/datahearth/streamline/releases/latest/download/streamline_<version>_linux_amd64.tar.gz
tar xzf streamline.tar.gz
mkdir -p ~/.config/streamline
cp config.example.yaml ~/.config/streamline/config.yaml
./streamline --config ~/.config/streamline/config.yamlEach archive ships a config.example.yaml with every key at its default value.
# /etc/systemd/system/streamline.service
[Unit]
Description=Streamline
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=streamline
Group=streamline
ExecStart=/opt/streamline/streamline --config /etc/streamline/config.yaml
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.targetsudo useradd --system --home /var/lib/streamline --shell /usr/sbin/nologin streamline
sudo mkdir -p /var/lib/streamline /etc/streamline
sudo chown -R streamline:streamline /var/lib/streamline
sudo systemctl enable --now streamline
sudo journalctl -u streamline -fSet data_dir: /var/lib/streamline in your config so the database lands somewhere the service user can write.
There's no first-party app-store package for these yet — you install the Docker image through whatever container UI your NAS provides.
Settings translation table and per-NAS notes
The settings translate like this:
| Field in your NAS's Docker UI | Value |
|---|---|
| Repository / Image | ghcr.io/datahearth/streamline:latest |
| Port |
8080 → 8080
|
| Volume: config | host …/streamline/config → container /etc/streamline
|
| Volume: data | host …/streamline/data → container /data
|
| Volume: media + downloads |
one host share (e.g. /mnt/user/data) → same path in container |
| Restart policy | Unless stopped |
Unraid specifics. Use /mnt/user/data as the single mount rather than separate /mnt/user/media and /mnt/user/downloads shares — the folder rule above applies with full force, and Unraid users hit it constantly. Set the container path identical to the host path so paths reported by your torrent client resolve unchanged.
Synology specifics. Container Manager (or Docker on older DSM) works fine. Create one shared folder holding both media and downloads subfolders. Note that Synology's Btrfs shares do support hardlinks, but only within a single shared folder — across two shared folders they'll fail.
TrueNAS SCALE specifics. Either use the Docker/Apps custom-app flow with the same mounts, or install the Helm chart directly since SCALE runs Kubernetes underneath.
You'll then need to generate a config file. The simplest route is to start the container once, let it fail or come up on defaults, then edit config/config.yaml on the NAS filesystem directly and restart.
helm install streamline oci://ghcr.io/datahearth/charts/streamline \
--namespace streamline --create-namespace \
--set image.tag=X.Y.ZPin a chart version with --version X.Y.Z.
image.tag is required (chart 2.2.0+): the chart never picks a Streamline version for you — you choose the app release to deploy and bump it yourself to upgrade. Installing without it fails with image.tag is required. App releases are tagged vX.Y.Z (the image tag is X.Y.Z), chart releases chart-vX.Y.Z.
The chart is versioned independently of Streamline itself: a chart fix ships without an app release and vice versa. --version selects the chart; image.tag selects the app.
Two things about the chart that surprise people:
-
replicaCountis 1 and must stay 1. Streamline stores state in SQLite, which is a single-writer database. A second replica will corrupt it. -
The chart defaults to
read_only: true. Config changes flow through git, not the web UI; the settings pages will reject writes. This is deliberate for declarative deploys — see GitOps and Kubernetes for the full treatment, including how to supply the secrets the app would normally generate for itself.
Streamline runs perfectly well with no ffmpeg or ffprobe anywhere on the machine. Three features
are gated behind them, and only those three:
| Feature | What you lose without ffprobe |
|---|---|
| Media info | Resolution, codecs, duration, channels, bitrate and stream languages on files and episodes. The rest of the page is unaffected |
| Import verification | Downloads are imported on the strength of the release name alone. With ffprobe, a file whose real resolution, duration or codec contradicts the claim is held for you to resolve instead of landing in your library |
| Scoring a file you already have | The audio_tracks, audio_language and subtitle_language conditions are answerable only from a probe. Without one they are unanswerable for a file on disk, so automatic upgrades compare the two sides on the parsed release name alone — see What a file can be scored on
|
Everything else — searching, grabbing, importing, renaming, requests, notifications — works identically either way. Missing binaries are a graceful degrade, never a boot error: nothing fails, nothing is retried forever, and imports simply fall back to filename parsing.
Where the binaries come from:
-
Docker / Helm — already there, nothing to do. The official image copies static
ffmpegandffprobebinaries into/usr/local/binfrom a digest-pinnedmwader/static-ffmpegstage, so the defaultffmpeg.path: ""finds them on$PATH -
Plain binary / from source — install them however your OS does (
apt install ffmpeg,brew install ffmpeg,pacman -S ffmpeg, …), or drop the two static binaries in a directory of your choosing
Turning it off, or pointing it elsewhere — two keys, both in Configuration Reference:
ffmpeg:
enabled: true # false disables probing entirely; runtime-editable, no restart
path: "" # a DIRECTORY holding ffmpeg/ffprobe. Empty resolves via $PATH. Read at bootIf probing is enabled but ffprobe can't be found, GET /api/v1/system/info returns
ffmpeg_warn: true, Settings → General shows a notice, and the header health pill goes amber.
That's the signal that path is wrong or the binaries aren't installed. With enabled: false the
key is absent: you opted out, so it isn't a warning.
Streamline ships a web-app manifest, so the browser can pin it to a home screen and open it full-screen without browser chrome. Nothing runs offline: it is the same web UI, just without the address bar.
- Android / desktop Chrome and Edge — open Streamline, then use the browser's install prompt (the icon in the address bar, or the browser menu → Install app).
- iOS Safari — Safari never prompts. Tap Share → Add to Home Screen.
The installed app uses the same session cookie as the tab, so a login in one is a login in both. If you sign in through OIDC, see the standalone note on the Authentication page.
Every release artefact is signed. If you care about supply-chain integrity, verify before running.
Binaries. checksums.txt is signed with cosign (keyless, via GitHub OIDC). Verify the signature first, then the hashes:
curl -fsSL -O https://github.com/datahearth/streamline/releases/latest/download/checksums.txt
curl -fsSL -O https://github.com/datahearth/streamline/releases/latest/download/checksums.txt.bundle
cosign verify-blob checksums.txt --bundle checksums.txt.bundle \
--certificate-identity-regexp="https://github.com/datahearth/streamline/.github/workflows/release.yaml@.*" \
--certificate-oidc-issuer=https://token.actions.githubusercontent.com
sha256sum -c checksums.txt --ignore-missingImages.
cosign verify ghcr.io/datahearth/streamline:latest \
--certificate-identity-regexp="https://github.com/datahearth/streamline/.github/workflows/image.yaml@.*" \
--certificate-oidc-issuer=https://token.actions.githubusercontent.comSBOMs. Each archive ships an SPDX SBOM alongside it (<archive>.sbom.spdx.json); images carry theirs as a cosign attestation:
cosign download attestation ghcr.io/datahearth/streamline:latest \
--predicate-type=https://spdx.dev/DocumentEvery image push is also scanned by grype at severity ≥ high, with results published to the repository's Security tab.
Next: First-Run Setup — logging in and connecting your indexers and download client.
🎬 Operating Streamline
- Installation
- First-Run Setup
- Adding Movies and TV
- Importing an Existing Library
- Activity and Calendar
- Requests and Users
- NixOS and Nix
- Troubleshooting
- Roadmap
⚙️ Advanced
- Configuration Reference
- Authentication and SSO
- Quality Profiles and Naming
- Quality Profiles and Custom Formats
- Scheduled Jobs
- REST API
- Observability and Logging
- GitOps and Kubernetes