-
Notifications
You must be signed in to change notification settings - Fork 33
Expand file tree
/
Copy pathDockerfile
More file actions
210 lines (187 loc) · 10.1 KB
/
Copy pathDockerfile
File metadata and controls
210 lines (187 loc) · 10.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
# =============================================================================
# Stage 1: Builder — compile native extensions and build wheels
# =============================================================================
FROM linuxserver/ffmpeg:8.1.2-cli-ls73 AS builder
ARG SETUPTOOLS_SCM_PRETEND_VERSION=""
# Build-time only: compiler toolchain + Python + git (for setuptools-scm)
RUN apt-get update && \
apt-get install -y --no-install-recommends \
gcc musl-dev python3 python3-pip git && \
rm -rf /var/lib/apt/lists/*
WORKDIR /build
ENV PIP_BREAK_SYSTEM_PACKAGES=1
# ---------------------------------------------------------------------------
# Layer A: pre-cache DEPENDENCY wheels via a stub package.
#
# This layer is keyed only on pyproject.toml. Without the split, every
# Python source change at the next layer would invalidate the wheel cache
# and force pip to rebuild ~50 dep wheels (Flask, plexapi, APScheduler,
# gunicorn, transitive deps) from scratch — adding 30-60s per build job
# (×4 parallel jobs in CI = ~30-60s wall-clock per push).
#
# The stub uses setuptools-scm's fallback_version so no git history is
# needed for this layer; the real version gets stamped at Layer B below.
# ---------------------------------------------------------------------------
COPY pyproject.toml ./
RUN mkdir -p media_preview_generator && \
touch media_preview_generator/__init__.py && \
SETUPTOOLS_SCM_PRETEND_VERSION_FOR_MEDIA_PREVIEW_GENERATOR=0.0.0 \
pip3 wheel --wheel-dir=/wheels --no-cache-dir . && \
# Drop the stub app wheel — Layer B builds the real one.
rm -f /wheels/media_preview_generator-*.whl && \
rm -rf media_preview_generator
# ---------------------------------------------------------------------------
# Layer B: build the APP wheel only, reusing the cached deps above.
#
# --no-deps tells pip to skip dep resolution (already pre-built).
# Source-only changes invalidate just this layer (~5-10s of work),
# keeping iteration speed high.
# ---------------------------------------------------------------------------
COPY media_preview_generator/ ./media_preview_generator/
RUN if [ -n "$SETUPTOOLS_SCM_PRETEND_VERSION" ]; then \
SETUPTOOLS_SCM_PRETEND_VERSION_FOR_MEDIA_PREVIEW_GENERATOR=$SETUPTOOLS_SCM_PRETEND_VERSION \
pip3 wheel --wheel-dir=/wheels --no-cache-dir --no-deps .; \
else \
pip3 wheel --wheel-dir=/wheels --no-cache-dir --no-deps .; \
fi
# =============================================================================
# Stage 2: Runtime — lean production image (no compiler toolchain)
# =============================================================================
FROM linuxserver/ffmpeg:8.1.2-cli-ls73
# Build metadata (optional; set via --build-arg in CI)
ARG GIT_BRANCH=unknown
ARG GIT_SHA=unknown
ARG BUILD_DATE=unknown
# Published image name baked in at build time. CI passes one of:
# - stevezzau/plex_generate_vid_previews (deprecated mirror; banner fires)
# - stevezzau/media_preview_generator (canonical name; no banner)
# - local (dev / unset; no banner)
# The runtime reads $DOCKER_IMAGE_NAME and surfaces an in-app deprecation
# notification when it matches the deprecated name.
ARG DOCKER_IMAGE_NAME=local
# Runtime dependencies. Split into two apt passes because we add two
# third-party repos before pulling jellyfin-ffmpeg + newer Intel drivers.
#
# GPU drivers for hardware acceleration:
# - Intel: intel-media-va-driver-non-free (modern Gen 8+), i965-va-driver (legacy Gen 5-9).
# Ubuntu Noble ships 24.1.0 which is 18 months old and missing DG2
# (Arc Alchemist) fixes. We pull 25.x from Intel's graphics apt repo
# on amd64 to get working VAAPI on Arc A380/A750/A770 + modern iGPUs.
# - AMD: mesa-va-drivers (AMD GPUs via VAAPI)
# - ARM/VideoCore: mesa-va-drivers (Mali GPUs, Raspberry Pi)
# - mesa-vulkan-drivers: Mesa's Vulkan ICDs (Intel ANV, AMD RADV, llvmpipe).
# Used by libplacebo DV5 tone mapping on NVIDIA and AMD. (Intel DV5 uses
# tonemap_opencl via intel-opencl-icd instead — libplacebo+Vulkan+Intel
# VAAPI has an upstream interop bug that returns VK_ERROR_OUT_OF_DEVICE_MEMORY
# on both iGPU and Arc discrete inside containers. See issue #212.)
# - intel-opencl-icd: Intel's OpenCL runtime for DV5 tone mapping via
# tonemap_opencl (Jellyfin-ffmpeg's patched DV-aware code path).
# - libvpl2 / libmfx-gen1: Intel oneVPL runtime (QSV).
# - libva2, libva-drm2: VA-API libraries
# - vainfo: Tool to test/verify VA-API functionality
# - pciutils: Provides lspci for better GPU naming
# - git: For version detection when running from mounted git repository
# - mediainfo: For media file metadata
#
# ffmpeg: we ship jellyfin-ffmpeg (8.1.2) as /usr/lib/jellyfin-ffmpeg/ffmpeg.
# The base image also has an 8.1.2 build at /usr/local/bin/ffmpeg which we
# leave in place as a fallback (our code defaults to the Jellyfin binary
# because it carries the DV RPU-aware tonemap_opencl patches upstream lacks).
#
# Both must stay >= 8.1.2: earlier builds are vulnerable to CVE-2026-8461
# ("PixelSmash"), a heap OOB write in the MagicYUV decoder reachable from
# ordinary preview generation on an untrusted library file. jellyfin-ffmpeg7
# is NOT an option -- it stopped at 7.1.4-3 (2026-06-06), before the fix.
RUN apt-get update && \
apt-get install -y --no-install-recommends \
mediainfo python3 python3-pip gosu pciutils git curl gnupg ca-certificates \
mesa-va-drivers mesa-vulkan-drivers libva2 libva-drm2 vainfo && \
if [ "$(dpkg --print-architecture)" = "amd64" ]; then \
# Jellyfin apt repo (Ubuntu Noble) for jellyfin-ffmpeg8.
# --retry-all-errors so a flaky HTTP 403 from the repo (seen
# intermittently in CI) is retried instead of failing the build.
curl -fsSL --retry 5 --retry-delay 3 --retry-all-errors --retry-connrefused \
https://repo.jellyfin.org/jellyfin_team.gpg.key \
| gpg --dearmor -o /usr/share/keyrings/jellyfin.gpg && \
printf 'Types: deb\nURIs: https://repo.jellyfin.org/ubuntu\nSuites: noble\nComponents: main\nArchitectures: amd64\nSigned-By: /usr/share/keyrings/jellyfin.gpg\n' \
> /etc/apt/sources.list.d/jellyfin.sources && \
# Intel graphics apt repo (Ubuntu Noble) for newer VAAPI + OpenCL
curl -fsSL --retry 5 --retry-delay 3 --retry-all-errors --retry-connrefused \
https://repositories.intel.com/gpu/intel-graphics.key \
| gpg --dearmor -o /usr/share/keyrings/intel-graphics.gpg && \
echo "deb [arch=amd64 signed-by=/usr/share/keyrings/intel-graphics.gpg] https://repositories.intel.com/gpu/ubuntu noble unified" \
> /etc/apt/sources.list.d/intel-graphics.list && \
apt-get update && \
apt-get install -y --no-install-recommends \
jellyfin-ffmpeg8 \
intel-media-va-driver-non-free \
i965-va-driver \
libmfx-gen1 \
libvpl2 \
intel-opencl-icd \
ocl-icd-libopencl1; \
fi && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
# Install pre-built wheels from builder (no compiler needed)
COPY --from=builder /wheels /tmp/wheels
ENV PIP_BREAK_SYSTEM_PACKAGES=1
RUN pip3 install --no-cache-dir --no-index /tmp/wheels/*.whl \
--ignore-installed blinker && \
rm -rf /tmp/wheels
# Replace init-adduser with clean version (no branding)
COPY docker-init-user.sh /etc/s6-overlay/s6-rc.d/init-adduser/run
RUN chmod +x /etc/s6-overlay/s6-rc.d/init-adduser/run
# /dev/dri/by-path/ fixup — Intel NEO (OpenCL) enumerates GPUs only via
# /dev/dri/by-path, and NVIDIA Container Toolkit populates that directory
# only for NVIDIA cards. On mixed Intel+NVIDIA containers the Intel GPU
# is invisible to OpenCL without this (VAAPI still works). Idempotent /
# no-op on single-vendor and bare-metal setups. See the DV5 plan file
# for root-cause trace inside intel/compute-runtime source.
COPY docker-init-dri-by-path.sh /etc/s6-overlay/s6-rc.d/init-dri-by-path/run
RUN chmod +x /etc/s6-overlay/s6-rc.d/init-dri-by-path/run && \
echo oneshot > /etc/s6-overlay/s6-rc.d/init-dri-by-path/type && \
echo /etc/s6-overlay/s6-rc.d/init-dri-by-path/run \
> /etc/s6-overlay/s6-rc.d/init-dri-by-path/up && \
mkdir -p /etc/s6-overlay/s6-rc.d/init-dri-by-path/dependencies.d && \
touch /etc/s6-overlay/s6-rc.d/init-dri-by-path/dependencies.d/init-adduser && \
touch /etc/s6-overlay/s6-rc.d/init-device-perms/dependencies.d/init-dri-by-path && \
touch /etc/s6-overlay/s6-rc.d/user/contents.d/init-dri-by-path
# Set working directory
WORKDIR /app
# Expose build metadata to the app (non-secret)
ENV GIT_BRANCH=${GIT_BRANCH} \
GIT_SHA=${GIT_SHA} \
BUILD_DATE=${BUILD_DATE} \
DOCKER_IMAGE_NAME=${DOCKER_IMAGE_NAME}
# Copy application source (needed for Flask templates and static files)
COPY pyproject.toml ./
COPY media_preview_generator/ ./media_preview_generator/
# Copy wrapper script
COPY wrapper.sh /app/wrapper.sh
RUN chmod +x /app/wrapper.sh
# Persist build metadata as a JSON artifact too — startup logs read this
# alongside the env vars so a tag-drift incident is grep-able from
# `docker logs` ("Build: ..."). Falls back gracefully when build args
# aren't supplied.
RUN printf '{"branch": "%s", "sha": "%s", "built": "%s"}\n' \
"${GIT_BRANCH}" "${GIT_SHA}" "${BUILD_DATE}" \
> /app/build_info.json
# Default PUID/PGID (override with environment variables)
ENV PUID=1000 \
PGID=1000 \
ATTACHED_DEVICES_PERMS="/dev/dri -type c"
# Tell s6-overlay to preserve environment variables
ENV S6_KEEP_ENV=1
# Expose web UI port
EXPOSE 8080
# Health check for web UI — respects WEB_PORT env var
# Uses python3 stdlib instead of curl (one fewer binary dependency).
# exec replaces the shell so Docker kills the Python process on timeout (no orphans).
# timeout=5 ensures the process exits if the server is unresponsive.
HEALTHCHECK --interval=30s --timeout=10s --start-period=15s --retries=3 \
CMD exec python3 -c "import urllib.request, os; urllib.request.urlopen('http://localhost:' + os.environ.get('WEB_PORT', '8080') + '/api/health', timeout=5)"
# Use LinuxServer's /init for PUID/PGID handling
ENTRYPOINT ["/init", "/app/wrapper.sh"]
# Web UI only; configure via the browser at http://<host>:<port>
CMD []