HandBrake for Unraid: the full video transcoder in your browser, with an automated watch folder.
10K+
A modern, plug-and-play Docker image for HandBrake on Unraid. The full
transcoder GUI in your browser via Selkies, dark by default using
HandBrake's own native GTK dark mode, plus a watch-folder converter that
transcodes anything you drop into /watch without opening the UI at
all. Everything is configurable from the Unraid template, no SSH or config-file
editing required.
A one-knight job: I build it, keep it running, work through the issues and add what people ask for, until nothing is missing. It is free, with no accounts, no telemetry, no ads and no paid tier. No asterisk anywhere. Nothing readable ever leaves your own walls. Forged on evenings and weekends, with heart and stubbornness.
If it has earned a place on your server or computer, toss a coin to your knight: it helps cover the costs and keeps the project alive. It also makes this knight's heart beat a little faster. Three ways below, whichever suits you.
This image packages HandBrake, the open-source video
transcoder, into a self-contained Docker container that runs in any modern web
browser. It is built on
linuxserver/baseimage-selkies,
so it inherits LSIO's actively maintained Selkies desktop-streaming stack (a
hybrid VNC/H.264 pipeline) and weekly security updates, while everything
HandBrake-specific is layered on top here.
What you get beyond bare HandBrake:
/watch, get a transcode in
/output, no GUI interaction.partial file and
renamed on success, so a media scanner never indexes a half-written videoAnother HandBrake container is also in Community Applications:
linuxserver/handbrake,
the official LinuxServer.io image. It builds on the very same
Docker Baseimage Selkies
project this image does, just the Arch Linux flavour of it instead of the
Ubuntu one, and opts into that base's newer Wayland/PixelFlux screen-streaming
pipeline (PIXELFLUX_WAYLAND=true) rather than the classic X11 one this image
still uses by default. A strong, actively developed GUI, but no automated
conversion of any kind, so it does not compete on the feature this image is
built around.
| This image | jlesage/handbrake | linuxserver/handbrake | |
|---|---|---|---|
| Web stack | Selkies (X11) | noVNC | Selkies (Wayland) |
| Base | Ubuntu (glibc) | Alpine (musl) | Arch Linux |
| NVIDIA NVENC encoding | ✅ | ❌ (#49) | n/a |
| Intel Quick Sync (QSV) encoding | ✅ | ⚠️ (#459) | n/a |
| AMD VCE encoding | ⚠️ unverified | ❌ (#441) | n/a |
| Dark mode default | ✅ | opt-in (DARK_MODE=1) | ❓ |
| Watch-folder conversion | ✅ | ✅ | ❌ |
| Browser clipboard | ✅ | ⚠️ | ✅ |
| File upload via WebUI | ✅ | ❌ | ✅ |
| Web file manager | ✅ | opt-in (WEB_FILE_MANAGER=1) | ❌ |
| Conversion hooks | ✅ | ✅ | ❌ |
| Staging on a separate disk | ✅ | ❌ | n/a |
| Shared-watch-folder locking | ✅ | ✅ | n/a |
| CJK fonts | ✅ | opt-in (ENABLE_CJK_FONT=1) | ❓ |
| Multi-arch | ✅ | ✅ | ❌ amd64 only |
| Direct VNC client | ❌ | ✅ | ❌ |
✅ works · ❌ doesn't · ⚠️ present but limited · ❓ undocumented · n/a no automated conversion to accelerate/configure
Unraid: install from Community Applications and adjust the paths in the template. Everything else has a working default.
Plain Docker:
docker run -d \
--name=handbrake \
-p 3000:3000 \
-p 3001:3001 \
-e PUID=99 \
-e PGID=100 \
-e TZ=Europe/Vienna \
-v /mnt/user/appdata/handbrake:/config \
-v /mnt/user/media:/storage:ro \
-v /mnt/user/media/watch:/watch \
-v /mnt/user/media/converted:/output \
--restart unless-stopped \
ghcr.io/junkerderprovinz/handbrake:latest
Then open https://<host>:3001/. Wait for HANDBRAKE IS READY in the container
log on the very first start.
| Container path | Mode | Purpose |
|---|---|---|
/config | rw | HandBrake presets, queue, logs and container state |
/storage | ro | Media you want to browse from inside the GUI |
/watch | rw | Watch folder; anything dropped here is converted automatically |
/watch2 … /watch5 | rw | Additional watch folders (optional) |
/output | rw | Where converted files are written |
| Port | Purpose |
|---|---|
3000 | WebUI over HTTP |
3001 | WebUI over HTTPS (self-signed by default) |
| Variable | Default | Description |
|---|---|---|
PUID / PGID | 911 | User and group the container runs as (Unraid: 99 / 100) |
UMASK | 000 | File-mode mask for everything the container creates. Keeps new files writable for other containers on the same shares |
TZ | Etc/UTC | Container timezone |
LANG | en_US.UTF-8 | Locale, also drives HandBrake's UI language |
HANDBRAKE_THEME | dark | dark or light, see Dark Mode |
APP_NICENESS | 0 | nice level (0-19) for the GUI and every transcode |
KEYBOARD_LAYOUT | us | X keyboard layout loaded at session start |
GPU_VENDOR | none | none, nvidia, intel or amd, see Hardware Encoding |
CUSTOM_USER / PASSWORD | empty | Set both to require a login on the WebUI; empty means no login |
CUSTOM_PORT / CUSTOM_HTTPS_PORT | 3000 / 3001 | Internal WebUI ports |
INSTALL_LIBDVDCSS | false | true builds libdvdcss on first start for encrypted DVDs, see Optical Drives |
The desktop follows your browser window: Selkies resizes the screen to the size the browser reports, so there is no screen size to set and memory only grows with the window you actually use. A 1600x1000 window on a laptop set to 200 % counts as 1600x1000. With HiDPI switched on in the Selkies sidebar the same window counts in physical pixels, 3200x2000.
Every file dropped into /watch (and /watch2…/watch5 when mounted) is
transcoded with the configured preset and written to /output. The variable
names match jlesage/handbrake so existing template values keep working.
| Variable | Default | Description |
|---|---|---|
AUTOMATED_CONVERSION | 1 | Set to 0 to disable the daemon entirely |
AUTOMATED_CONVERSION_PRESET | General/Very Fast 1080p30 | HandBrake preset, category/name |
AUTOMATED_CONVERSION_FORMAT | mp4 | Output container: mp4, mkv or webm |
AUTOMATED_CONVERSION_KEEP_SOURCE | 1 | 0 deletes the source after a successful conversion |
AUTOMATED_CONVERSION_VIDEO_FILE_EXTENSIONS | (built-in list) | Space-separated extensions to pick up |
AUTOMATED_CONVERSION_WATCH_DIR | AUTO | AUTO scans /watch…/watchN; any other value is used as the single watch folder |
AUTOMATED_CONVERSION_MAX_WATCH_FOLDERS | 5 | How many /watchN folders AUTO looks for |
AUTOMATED_CONVERSION_OUTPUT_DIR | /output | Destination folder |
AUTOMATED_CONVERSION_OUTPUT_SUBDIR | empty | A fixed subfolder, or SAME_AS_SRC to mirror the source tree |
AUTOMATED_CONVERSION_OVERWRITE_OUTPUT | 0 | 1 overwrites an existing output file |
AUTOMATED_CONVERSION_SOURCE_STABLE_TIME | 5 | Seconds a file must stop changing before it is picked up |
AUTOMATED_CONVERSION_CHECK_INTERVAL | 5 | Seconds between watch-folder scans |
AUTOMATED_CONVERSION_HANDBRAKE_CUSTOM_ARGS | empty | Extra HandBrakeCLI arguments appended to every job |
AUTOMATED_CONVERSION_STAGING_DIR | empty | Where in-progress conversions are written. Empty means <output>/.handbrake-staging |
AUTOMATED_CONVERSION_IGNORE_DIRECTORIES | empty | Space-separated directory basenames pruned from every watch-folder scan, matched anywhere in the tree |
AUTOMATED_CONVERSION_ACTIVE_HOURS | empty | Restrict conversion to a daily window, HH-HH (24h clock, e.g. 22-06 for overnight only). Empty means always active. Uses the container's TZ (default Etc/UTC), so set TZ first if the window should follow your local time |
How it behaves:
AUTOMATED_CONVERSION_SOURCE_STABLE_TIME seconds, so a file still being
copied in is never touched.HandBrakeCLI succeeds, so a media scanner watching /output
never sees a half-written file. By default the staging directory is a hidden
folder under the output root (<output>/.handbrake-staging). Map /staging
to a cache pool and set AUTOMATED_CONVERSION_STAGING_DIR=/staging to keep the
array out of the write path while a transcode runs. When staging and output are
on different filesystems the finished file is copied to a hidden sibling inside
the output folder first and then renamed, so the last step stays atomic./config/handbrake/watch-state/done.list by path, size and mtime, so an
unchanged source is never converted twice, an edited or re-copied one is.failed.list and is not retried until the source
changes. The full HandBrakeCLI output for every job is in
/config/handbrake-watch.log.HANDBRAKE_THEME=dark (the default) applies HandBrake's own native GTK dark
mode, the stock Adwaita dark theme that ships inside GTK 4, exactly what
HandBrake uses on any Linux desktop set to dark. Nothing is repainted or
restyled. HANDBRAKE_THEME=light switches to the light variant.
One consequence worth knowing: the container sets GTK_THEME, which GTK reads
before it looks at any in-app preference. HandBrake's own light/dark toggle in
the UI therefore has no visible effect here; HANDBRAKE_THEME is the single
source of truth. Change it in the template and restart the container.
Set GPU_VENDOR and pass the device through; the watch-folder converter then
adds --encoder <hardware encoder> to every job. The GUI is unaffected and
keeps its own encoder dropdown. This is the feature the Alpine-based
community image has never been able to ship at all: NVIDIA's userspace
libraries are glibc binaries that musl cannot load, so its NVENC request has
been open since 2019.
This image is Ubuntu-based, so the standard container runtimes just work.
GPU_VENDOR | What you need on the host | Works out of the box | Verified by the maintainer |
|---|---|---|---|
none (default) | nothing | ✅ software x264/x265 | ✅ |
nvidia | --runtime=nvidia, the Nvidia-Driver plugin | ✅ | ✅ real hardware, RTX 4070 Ti SUPER |
intel | /dev/dri passthrough, i915 or xe kernel driver | ❌ known Ubuntu-packaging bug | ✅ real hardware, bug found and fixed (handbrake:gpu-full) |
amd | /dev/dri passthrough, amdgpu kernel driver, a custom image, AMD's AMF runtime | ❌ see below | ❌ no AMD GPU here |
The container never pretends. If the encoder you asked for is not usable it
falls back to software and writes the reason into the container log, plus a
full report to /config/handbrake-gpu.log.
GPU_VENDOR=nvidia encodes every watch-folder job on an NVIDIA GPU using
HandBrake's NVENC encoder instead of the CPU.
Status: developer-verified on real hardware (NVIDIA GeForce RTX 4070 Ti SUPER, Unraid).
| Requirement | Value |
|---|---|
| Unraid plugin | Nvidia-Driver (ich777), from Community Applications |
| Extra Parameters | --runtime=nvidia |
NVIDIA_VISIBLE_DEVICES | a GPU UUID from nvidia-smi -L on the host, or all |
NVIDIA_DRIVER_CAPABILITIES | compute,video,utility (or all) |
GPU_VENDOR | nvidia |
NVIDIA_DRIVER_CAPABILITIES matters more than it looks: with the variable unset
the NVIDIA runtime defaults to utility,compute, which does not include
video, and video is the capability that injects libnvidia-encode.so.1,
the library NVENC actually calls.
Plain Docker:
docker run -d \
--name=handbrake \
--runtime=nvidia \
-e NVIDIA_VISIBLE_DEVICES=all \
-e NVIDIA_DRIVER_CAPABILITIES=compute,video,utility \
-e GPU_VENDOR=nvidia \
-p 3000:3000 -p 3001:3001 \
-e PUID=99 -e PGID=100 -e TZ=Europe/Vienna \
-v /mnt/user/appdata/handbrake:/config \
-v /mnt/user/media/watch:/watch \
-v /mnt/user/media/converted:/output \
--restart unless-stopped \
ghcr.io/junkerderprovinz/handbrake:latest
GPU_VENDOR adds --encoder nvenc_h264 to every
automated conversion. In the GUI you pick the encoder yourself; the NVENC
entries appear in HandBrake's own encoder list as soon as the GPU is passed in.nvenc_h264 keeps the delivered codec identical and only swaps the encoder.
For HEVC, set
AUTOMATED_CONVERSION_HANDBRAKE_CUSTOM_ARGS=--encoder nvenc_h265; custom args
are appended last, so they win. nvenc_av1 and nvenc_av1_10bit are compiled
in too (confirmed in docs/hardware-encoding-nvidia.md); the watch-folder seam
does not pick AV1 automatically since not every player supports it yet, but
--encoder nvenc_av1 works the same way.AUTOMATED_CONVERSION_PRESET already names an NVENC preset, the container does
not override its encoder.veryfast; HandBrake substitutes its own default. To control the tradeoff
yourself, add --encoder-preset <name> to the custom args; the valid names
are listed by
docker exec handbrake HandBrakeCLI --encoder-preset-list nvenc_h264.AUTOMATED_CONVERSION_HANDBRAKE_CUSTOM_ARGS=--enable-hw-decoding nvdec if your
preset has no filters.The measured details for this build, including the NVENC encoders it offers on
a working GPU and the hardware evidence behind the "developer-verified" claim,
are in docs/hardware-encoding-nvidia.md.
Requirements on the host:
--device=/dev/dri
to Extra Parameters. Plain Docker: --device /dev/dri.i915 (or xe) kernel driver, which every current Linux
kernel ships. No proprietary driver, no vendor container toolkit, nothing to
install on the host.docker run -d \
--name=handbrake \
--device /dev/dri \
-e GPU_VENDOR=intel \
... \
ghcr.io/junkerderprovinz/handbrake:latest
On the next start the log says which encoder was chosen:
[handbrake-gpu] Intel QSV enabled: --encoder qsv_h264 (render node /dev/dri/renderD128)
Important: the default image's QSV detection is correct, but the encode
itself currently fails. This was measured on real Intel hardware (Intel UHD
770), not assumed: every conversion with the stock, apt-installed
HandBrakeCLI fails at the muxing step with "Application provided invalid,
non monotonically increasing dts to muxer". This is a confirmed bug in
Ubuntu's specific packaged build of HandBrake, independently reproduced by
another user on the identical environment
(HandBrake/HandBrake#7962),
not a bug in this container or in HandBrake itself.
The fix ships as an optional variant image. Build handbrake:gpu-full
(Dockerfile.gpu, just build-gpu-full, 30-60 minutes on 8 cores, amd64
only, not published). It rebuilds HandBrakeCLI from source with
--enable-qsv, which fixes the bug completely. Verified end to end: a 180 s
1080p30 clip encodes in 12 s with qsv_h264 on this hardware (27 s in
software on the same CPU), with no mux errors, and the output decodes
cleanly. Full measured evidence is in
docs/hardware-encoding-intel.md.
docker build -f Dockerfile.gpu -t handbrake:gpu-full .
docker run -d \
--name=handbrake \
--device /dev/dri \
-e GPU_VENDOR=intel \
... \
handbrake:gpu-full
Notes worth knowing:
qsv_h264, so the output codec matches what the default
preset produces and stays as compatible as before. For HEVC, set
AUTOMATED_CONVERSION_HANDBRAKE_CUSTOM_ARGS=--encoder qsv_h265; custom
arguments are appended after the automatic ones and always win.--enable-hw-decoding qsv to AUTOMATED_CONVERSION_HANDBRAKE_CUSTOM_ARGS if
you want it.docs/hardware-encoding-intel.md section 2.Honest summary: the stock image cannot do AMD hardware encoding, and this is
not something we can fix from inside the container. Setting GPU_VENDOR=amd
is still worth doing, because it prints the exact reason and keeps converting in
software:
[handbrake-gpu] WARNING: GPU_VENDOR=amd and /dev/dri/renderD128 exists, but HandBrakeCLI offers neither vce_h264 nor vaapi_h264 here.
Why:
--enable-vce, and upstream enables it by
default only for Windows builds, so the packaged HandBrakeCLI contains no
AMD encoder at all.Content type
Image
Digest
sha256:bbba22ac4…
Size
1013.6 MB
Last updated
10 days ago
docker pull junkerderprovinz/handbrake