Sign inSign up

junkerderprovinz/handbrake

By junkerderprovinz

•Updated 1 day ago

HandBrake for Unraid: the full video transcoder in your browser, with an automated watch folder.

Image
0

10K+

junkerderprovinz/handbrake repository overview

HandBrake for Unraid

Build  Lint  Docker Pulls  Image Size  Arch  Selkies  Unraid  License: AGPL-3.0


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.

Buy me a coffee   PayPal   Donate with crypto


⁠Table of Contents

  1. Overview⁠
  2. Screenshots⁠
  3. Quick Start⁠
  4. Volumes and Ports⁠
  5. Configuration⁠
  6. Automated Watch-Folder Conversion⁠
  7. Dark Mode⁠
  8. Hardware Encoding⁠
  9. Conversion Hooks⁠
  10. Web Desktop Features⁠
  11. Running More Than One Instance⁠
  12. Optical Drives⁠
  13. Migrating from jlesage/handbrake⁠
  14. Building Locally⁠
  15. Troubleshooting⁠
  16. License⁠
  17. How AI is used here⁠
  18. Support this project⁠

⁠1. Overview

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:

  • Selkies instead of noVNC: a hybrid VNC/H.264 pipeline for a smooth web desktop, real bidirectional browser clipboard, native file upload and download, high-DPI ready
  • Dark by default: HandBrake's own native GTK dark mode, not a repaint; switch to light with one variable
  • Watch-folder automation: drop a file into /watch, get a transcode in /output, no GUI interaction
  • Atomic output: conversions are written to a hidden .partial file and renamed on success, so a media scanner never indexes a half-written video
  • Multi-arch: amd64 and arm64, both gated by a CI smoke test that really transcodes a clip before anything is published

Another 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 imagejlesage/handbrakelinuxserver/handbrake
Web stackSelkies (X11)noVNCSelkies (Wayland)
BaseUbuntu (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


⁠2. Screenshots

HandBrake running in the browser in dark mode


⁠3. Quick Start

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.


⁠4. Volumes and Ports

Container pathModePurpose
/configrwHandBrake presets, queue, logs and container state
/storageroMedia you want to browse from inside the GUI
/watchrwWatch folder; anything dropped here is converted automatically
/watch2 … /watch5rwAdditional watch folders (optional)
/outputrwWhere converted files are written
PortPurpose
3000WebUI over HTTP
3001WebUI over HTTPS (self-signed by default)

⁠5. Configuration

VariableDefaultDescription
PUID / PGID911User and group the container runs as (Unraid: 99 / 100)
UMASK000File-mode mask for everything the container creates. Keeps new files writable for other containers on the same shares
TZEtc/UTCContainer timezone
LANGen_US.UTF-8Locale, also drives HandBrake's UI language
HANDBRAKE_THEMEdarkdark or light, see Dark Mode⁠
APP_NICENESS0nice level (0-19) for the GUI and every transcode
KEYBOARD_LAYOUTusX keyboard layout loaded at session start
GPU_VENDORnonenone, nvidia, intel or amd, see Hardware Encoding⁠
CUSTOM_USER / PASSWORDemptySet both to require a login on the WebUI; empty means no login
CUSTOM_PORT / CUSTOM_HTTPS_PORT3000 / 3001Internal WebUI ports
INSTALL_LIBDVDCSSfalsetrue builds libdvdcss on first start for encrypted DVDs, see Optical Drives⁠
⁠Screen size and memory use

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.


⁠6. Automated Watch-Folder Conversion

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.

VariableDefaultDescription
AUTOMATED_CONVERSION1Set to 0 to disable the daemon entirely
AUTOMATED_CONVERSION_PRESETGeneral/Very Fast 1080p30HandBrake preset, category/name
AUTOMATED_CONVERSION_FORMATmp4Output container: mp4, mkv or webm
AUTOMATED_CONVERSION_KEEP_SOURCE10 deletes the source after a successful conversion
AUTOMATED_CONVERSION_VIDEO_FILE_EXTENSIONS(built-in list)Space-separated extensions to pick up
AUTOMATED_CONVERSION_WATCH_DIRAUTOAUTO scans /watch…/watchN; any other value is used as the single watch folder
AUTOMATED_CONVERSION_MAX_WATCH_FOLDERS5How many /watchN folders AUTO looks for
AUTOMATED_CONVERSION_OUTPUT_DIR/outputDestination folder
AUTOMATED_CONVERSION_OUTPUT_SUBDIRemptyA fixed subfolder, or SAME_AS_SRC to mirror the source tree
AUTOMATED_CONVERSION_OVERWRITE_OUTPUT01 overwrites an existing output file
AUTOMATED_CONVERSION_SOURCE_STABLE_TIME5Seconds a file must stop changing before it is picked up
AUTOMATED_CONVERSION_CHECK_INTERVAL5Seconds between watch-folder scans
AUTOMATED_CONVERSION_HANDBRAKE_CUSTOM_ARGSemptyExtra HandBrakeCLI arguments appended to every job
AUTOMATED_CONVERSION_STAGING_DIRemptyWhere in-progress conversions are written. Empty means <output>/.handbrake-staging
AUTOMATED_CONVERSION_IGNORE_DIRECTORIESemptySpace-separated directory basenames pruned from every watch-folder scan, matched anywhere in the tree
AUTOMATED_CONVERSION_ACTIVE_HOURSemptyRestrict 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:

  • A file is only converted once it has been stable for AUTOMATED_CONVERSION_SOURCE_STABLE_TIME seconds, so a file still being copied in is never touched.
  • Output is written into the staging directory and only moved to its final place after 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.
  • If the staging directory cannot be written, the daemon says so loudly and refuses to convert anything instead of failing every file one by one. The GUI keeps working.
  • Processed sources are remembered in /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.
  • A failed job is recorded in failed.list and is not retried until the source changes. The full HandBrakeCLI output for every job is in /config/handbrake-watch.log.

⁠7. Dark Mode

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.


⁠8. Hardware Encoding

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_VENDORWhat you need on the hostWorks out of the boxVerified 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.

⁠NVIDIA NVENC

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).

⁠What the host needs
RequirementValue
Unraid pluginNvidia-Driver (ich777), from Community Applications
Extra Parameters--runtime=nvidia
NVIDIA_VISIBLE_DEVICESa GPU UUID from nvidia-smi -L on the host, or all
NVIDIA_DRIVER_CAPABILITIEScompute,video,utility (or all)
GPU_VENDORnvidia

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
⁠What it changes
  • Watch-folder jobs only. 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.
  • H.264 by default, on purpose. The default preset is an x264 preset, so 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.
  • A HandBrake hardware preset is left alone. If AUTOMATED_CONVERSION_PRESET already names an NVENC preset, the container does not override its encoder.
  • Speed presets. NVENC does not understand x264 speed names such as 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.
  • Hardware decoding (NVDEC) stays off. HandBrake disables hardware decoding as soon as any filter runs, and every stock preset crops or scales, so it would buy nothing by default. Force it with 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⁠.

⁠Intel Quick Sync (QSV)

Requirements on the host:

  • The iGPU or Arc card passed into the container. Unraid: add --device=/dev/dri to Extra Parameters. Plain Docker: --device /dev/dri.
  • The open-source 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:

  • The chosen encoder is 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.
  • Hardware decoding is not enabled automatically. Add --enable-hw-decoding qsv to AUTOMATED_CONVERSION_HANDBRAKE_CUSTOM_ARGS if you want it.
  • Quality is preset-driven and the RF scale is not identical between x264 and QSV, so expect a different file size at the same nominal quality.
  • Quick Sync is x86-64 only. On arm64 the variable is accepted and ignored, with a log line saying so.
  • Which encoders your specific GPU generation actually supports (H.264/H.265 are broadly supported; AV1 needs a newer generation) is logged by HandBrake itself at the start of every job, see docs/hardware-encoding-intel.md⁠ section 2.
⁠AMD VCE

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:

  • HandBrake's AMD path on Linux is VCE through AMD's AMF framework. Ubuntu does not build HandBrake with --enable-vce, and upstream enables it by default only for Windows builds, so the packaged HandBrakeCLI contains no AMD encoder at all.
  • Even a rebuilt binary needs AMD

Tag summary

Content type

Image

Digest

sha256:bbba22ac4…

Size

1013.6 MB

Last updated

10 days ago

docker pull junkerderprovinz/handbrake