Dockerfile
Rama main · réplica de lectura del repositorio original.
# ---- Build stage ----
# Pin the builder to the host's native arch and cross-compile (CGO is off, so
# Go cross-compiles trivially). During multi-arch buildx this keeps `go build`
# at native speed instead of compiling under QEMU emulation for the foreign arch.
# Must match the go directive in go.mod: the image runs GOTOOLCHAIN=local, so an
# older builder fails `go mod download` (v1.11.10 shipped without an image for
# exactly this). CI's "Dockerfile Go matches go.mod" step enforces it.
FROM --platform=$BUILDPLATFORM golang:1.26-alpine AS builder
RUN apk add --no-cache git ca-certificates
WORKDIR /src
# Copy go.mod/go.sum first for layer caching. third_party/ comes along because
# go.mod `replace`s anacrolix/torrent with ./third_party/torrent: without it
# `go mod download` cannot read that module's go.mod.
COPY go.mod go.sum ./
COPY third_party ./third_party
RUN go mod download
# Copy source
COPY . .
ARG VERSION=dev
ARG TARGETOS
ARG TARGETARCH
RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -ldflags="-s -w -X github.com/Unarr-app/unarr-cli/internal/cmd.Version=${VERSION}" -trimpath -o /unarr ./cmd/unarr/
# ---- Runtime stage ----
# glibc base (not Alpine/musl). NVIDIA's userspace — nvidia-smi and the
# libnvidia-encode / libcuda libs that `--gpus all` injects, plus the static
# BtbN ffmpeg that links nvenc — are all glibc ELF. On musl they fail with
# "no such file or directory" (missing glibc loader), so HW transcode is
# impossible on Alpine. bookworm-slim is the smallest base that runs the full
# NVIDIA stack while still falling back to software libx264 when no GPU is
# passed in.
FROM debian:bookworm-slim
# par2 → repair corrupted Usenet segments (without it a single bad segment
# silently corrupts the output).
# 7z → archive extractor for RAR/7z-packed downloads (p7zip-full also reads
# RAR5, so unrar — unavailable as a free Debian package — isn't needed).
# tzdata/ca-certificates → TLS + correct local time for schedules/logs.
# libvulkan1 → the Vulkan loader (libvulkan.so.1). ffmpeg's libplacebo filter
# (GPU HDR→SDR tonemap) loads Vulkan dynamically through it; without the
# loader the filter can't reach a GPU even when the NVIDIA driver mounts
# its ICD. ~150 KB. The agent only USES libplacebo after a functional
# probe (FFmpegSupportsLibplacebo) succeeds AND a real HW encoder is
# present, so this is inert on hosts without a working Vulkan GPU.
#
# NOTE: in this container libplacebo's Vulkan probe ALWAYS fails and the
# agent falls back to the CPU zscale tonemap chain — by design, not a
# bug. The nvidia Vulkan ICD is libGLX_nvidia.so.0, whose GL backend
# (libnvidia-glcore) references glibc malloc hooks removed in glibc 2.34
# (__malloc_hook/__free_hook/...) and the Xorg symbol ErrorF; on a
# headless modern-glibc base (debian or ubuntu) those go unresolved so
# vkCreateInstance returns VK_ERROR_INCOMPATIBLE_DRIVER. We deliberately
# do NOT chase it (would need `graphics` cap + X11 libs + a 1.4 loader
# AND a desktop-class glibc/Xorg — fragile, distro+driver coupled). The
# loader stays so that on the RARE host where Vulkan does come up the
# probe can use it. nvenc/nvdec (CUDA, not Vulkan) work regardless.
# GPU HDR tonemap is a bare-metal-binary feature, not a container one.
# libdrm2/libva2/libva-drm2 → VAAPI/QSV userspace the bundled BtbN ffmpeg
# dlopen()s for Intel QuickSync (h264_qsv/hevc_qsv). Without libdrm.so.2
# the qsv encoder aborts on the very first frame ("libdrm.so.2: cannot
# open shared object file" → `load_library: Assertion 0` → core dump), so
# an Intel-GPU host (renderD128) never produces a segment and web
# playback hangs forever on init.mp4. All three exist on arm64 too and
# are inert there, so they live on the common line; the Intel-only driver
# + oneVPL runtime are installed in an amd64-guarded block below.
# (No gosu: docker-entrypoint.sh drops root with setpriv from util-linux, which
# is Essential in Debian and keeps the supplementary groups gosu discarded.)
RUN apt-get update && \
apt-get install -y --no-install-recommends \
ca-certificates tzdata wget xz-utils par2 p7zip-full libvulkan1 \
libdrm2 libva2 libva-drm2 && \
rm -rf /var/lib/apt/lists/*
# Intel QuickSync (QSV) runtime — amd64 only. The oneVPL dispatcher (libvpl2) +
# the Gen GPU runtime (libmfx-gen1.2) are what the bundled ffmpeg loads for
# h264_qsv/hevc_qsv; intel-media-va-driver-non-free is the iHD VAAPI driver
# covering Gen9+ incl the Alder Lake-N SoCs that UGREEN / Intel NAS boxes ship.
# These packages have no arm64 candidate (Intel x86 media stack), so installing
# them on the common line would break the arm64 buildx leg — guard by arch. iHD
# lives in non-free, absent from bookworm-slim's default sources, so enable the
# component first. Inert on NVIDIA hosts (which use nvenc, not QSV).
# The non-free stanza reuses the base image's deb822 keyring (Signed-By); a plain
# one-line `deb ...` list for the same suite trips apt's "Conflicting values set
# for option Signed-By" on modern bookworm-slim (which ships /etc/apt/sources.list.d
# /debian.sources with the keyring pinned), so it must be deb822 with the same key.
RUN if [ "$(dpkg --print-architecture)" = "amd64" ]; then \
printf 'Types: deb\nURIs: http://deb.debian.org/debian\nSuites: bookworm\nComponents: non-free non-free-firmware\nSigned-By: /usr/share/keyrings/debian-archive-keyring.gpg\n' \
> /etc/apt/sources.list.d/non-free.sources && \
apt-get update && \
apt-get install -y --no-install-recommends \
intel-media-va-driver-non-free libmfx-gen1.2 libvpl2 && \
rm -rf /var/lib/apt/lists/*; \
fi
# Arch for the bundled deps below is taken from `dpkg --print-architecture` (the
# real arch of THIS runtime stage), NOT the TARGETARCH build-arg. A baked
# `ARG TARGETARCH=amd64` default used to shadow buildx's per-leg value in this
# stage, so even the published arm64 image bundled an amd64 cloudflared/ffmpeg
# while the unarr binary was native arm64 → "exec format error" when the daemon
# spawned cloudflared → funnel never came up → TV/Stremio connect failed
# ("Failed to get add-on manifest"). dpkg reads the emulated base image's arch,
# so it is correct under buildx cross-builds AND a plain `docker build`.
# Static GPL ffmpeg + ffprobe with nvenc compiled in (BtbN builds). nvenc is
# linked but the actual libnvidia-encode.so is dlopen'd at runtime from the
# host driver that `--gpus all` exposes — so the same binary does HW transcode
# when a GPU is present and falls back to libx264 when it isn't. Placed in
# /usr/local/bin so ResolveFFmpeg picks them up off PATH ahead of any distro
# ffmpeg. arm64 has no nvenc but the build still serves software transcode.
RUN ARCH="$(dpkg --print-architecture)" && \
case "$ARCH" in \
amd64) FF_ARCH=linux64 ;; \
arm64) FF_ARCH=linuxarm64 ;; \
*) echo "unsupported arch=$ARCH" >&2; exit 1 ;; \
esac && \
wget -4 --tries=3 --timeout=30 -qO /tmp/ffmpeg.tar.xz "https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-${FF_ARCH}-gpl.tar.xz" && \
mkdir -p /tmp/ff && tar -xJf /tmp/ffmpeg.tar.xz -C /tmp/ff --strip-components=1 && \
cp /tmp/ff/bin/ffmpeg /tmp/ff/bin/ffprobe /usr/local/bin/ && \
chmod +x /usr/local/bin/ffmpeg /usr/local/bin/ffprobe && \
rm -rf /tmp/ffmpeg.tar.xz /tmp/ff
# Bundle cloudflared so `unarr funnel on` (default: on, see config defaults)
# Just Works on a headless container with no first-run network round-trip.
# Keep CF_VERSION and the hashes in step with internal/funnel/install.go.
ARG CF_VERSION=2026.10.5
RUN ARCH="$(dpkg --print-architecture)" && \
case "$ARCH" in \
amd64) CF_ARCH=amd64; CF_SHA=e1bc0cf084fd9e590554fe769a86b1db46701c3f847a43d902f7b974a4e16298 ;; \
arm64) CF_ARCH=arm64; CF_SHA=72e0d05a67e09da87cf495e409a28c3cc5994dd23fc66a982eebea6a55923b7d ;; \
armhf) CF_ARCH=armhf; CF_SHA=4aed2f4179f2ac1068b45073761409ba4540a631a2e70df98543a65b4a982d27 ;; \
*) echo "unsupported arch=$ARCH" >&2; exit 1 ;; \
esac && \
wget -4 --tries=3 --timeout=30 -qO /usr/local/bin/cloudflared "https://github.com/cloudflare/cloudflared/releases/download/$CF_VERSION/cloudflared-linux-$CF_ARCH" && \
echo "$CF_SHA /usr/local/bin/cloudflared" | sha256sum -c - && \
chmod +x /usr/local/bin/cloudflared
# Non-root user (UID 1000 matches typical host user for volume permissions)
RUN groupadd -g 1000 unarr && useradd -u 1000 -g 1000 -m -d /home/unarr unarr
# Default directories
RUN mkdir -p /config /downloads /data && \
chown -R unarr:unarr /config /downloads /data
# The container starts as root ONLY to chown the mounts, then execs as
# PUID:PGID (default 1000:1000 — identical to the old `USER unarr`). Baking the
# uid instead made every NAS fail: Synology uids start at 1024, unRAID uses
# 99:100, so the bind-mounted /config and /downloads were unwritable and the
# agent died on its first write. See docker-entrypoint.sh.
COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
COPY --from=builder /unarr /usr/local/bin/unarr
# Environment: point config/data to container paths
ENV UNARR_CONFIG_DIR=/config
ENV UNARR_DOWNLOAD_DIR=/downloads
ENV XDG_DATA_HOME=/data
# Mark this as a container install so the agent reports isDocker=true to the web
# (which then shows a `docker pull` command instead of the in-app update button —
# the binary self-update refuses to run in Docker). Covers podman/containerd too,
# which don't create /.dockerenv. See internal/agent/RunningInDocker.
ENV UNARR_DOCKER=1
# NVIDIA passthrough defaults. `--gpus all` alone only grants the "utility" +
# "compute" capabilities; nvenc needs "video", and "graphics" makes the runtime
# mount the NVIDIA Vulkan ICD (nvidia_icd.json — the load-bearing piece — plus
# GLX/EGL libs) so ffmpeg's libplacebo filter (GPU HDR tonemap, paired with
# libvulkan1 above) can create a Vulkan device. "compute" alone does NOT mount
# the ICD. Baking these here means a plain `docker run --gpus all` (or the compose
# device reservation) lights up HW transcode + GPU tonemap with zero extra flags.
# Harmless when no GPU is attached.
ENV NVIDIA_VISIBLE_DEVICES=all
ENV NVIDIA_DRIVER_CAPABILITIES=video,compute,utility,graphics
# Select the iHD VAAPI driver for Intel QuickSync (installed on amd64 above).
# Without this libva may probe the wrong/absent driver on some hosts; iHD is the
# modern Intel driver (Gen9+). Harmless on NVIDIA/arm64 hosts where no libva
# device is used (QSV simply isn't selected). Pass /dev/dri via `--device`.
ENV LIBVA_DRIVER_NAME=iHD
VOLUME ["/config", "/downloads", "/data"]
# Without this, a container whose daemon has died reports "running" forever:
# the entrypoint process is still alive, and nothing else looks at whether the
# thing it supervises is. Docker, Portainer, Swarm, k8s and `compose --wait`
# all read this and nothing else.
#
# --quick is the load-bearing flag. It runs ONLY the local checks — config,
# download dir, disk, daemon liveness — and makes NO network call. A
# healthcheck that reaches the API marks the container unhealthy on every
# transient blip, Docker restarts it, and one ISP hiccup becomes a restart loop
# across the fleet. Warnings never produce a non-zero exit for the same reason:
# Docker's only response to "unhealthy" is to restart, and a missing par2 is
# not a reason to restart anything.
#
# start-period is generous because the first boot registers the agent and scans
# the library, and neither is instant on a NAS.
HEALTHCHECK --interval=60s --timeout=15s --start-period=90s --retries=3 \
CMD unarr doctor --quick --json > /dev/null || exit 1
ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]
# `up` is `start` plus one extra ability: if UNARR_AUTHKEY is set it redeems the
# key first. With CMD=start, `docker run -e UNARR_AUTHKEY=… unarr/cli` ignored the
# key and died with "no API key configured" — the container has no tty for the
# wizard, so that was the end of the road. `up` with no key and a stored
# credential behaves exactly like `start`.
CMD ["up"]