The first Trivy scan of the shipped image (0.98.0) found four fixable HIGH advisories. One was pcre2: a Debian fix newer than the base snapshot, now taken by `apt-get upgrade` at build time. The others were urllib3 (bumped in the lock), and msgpack, setuptools and urllib3 vendored inside pip itself. pip is removed once it has installed the lock, since nothing runs it at runtime. A local build of this Dockerfile with --pull scans clean: 0 HIGH/CRITICAL with a fix, no secrets. .trivyignore.yaml is the publish gate's allowlist, empty today. Each entry must carry a statement and an expiry date.
105 lines
5.7 KiB
Docker
105 lines
5.7 KiB
Docker
# syntax=docker/dockerfile:1
|
|
# Build/version info. APP_VERSION comes from the committed VERSION file (always correct,
|
|
# even for a plain `docker compose build`); GIT_SHA/BUILD_DATE are best-effort build-args
|
|
# the deploy scripts pass. An explicit APP_VERSION build-arg still overrides the file.
|
|
#
|
|
# GIT_SHA/BUILD_DATE differ on EVERY build: declare them as late as possible in each stage —
|
|
# anything below them can never cache.
|
|
ARG APP_VERSION=
|
|
ARG GIT_SHA=unknown
|
|
ARG BUILD_DATE=
|
|
|
|
# Stage 1: build the frontend SPA
|
|
# The major is pinned to `.nvmrc` and held there by frontend/src/lib/nodeEngine.contract.test.ts:
|
|
# this image, the CI gate's image and the dev machine must be ONE engine (see that test for why).
|
|
FROM node:24-alpine AS frontend
|
|
WORKDIR /fe
|
|
# Deps first, keyed on the lockfile, so editing app source never re-resolves node_modules.
|
|
COPY frontend/package.json frontend/package-lock.json ./
|
|
RUN --mount=type=cache,target=/root/.npm npm ci
|
|
COPY frontend/ ./
|
|
COPY VERSION ./.version
|
|
# Version args land here — immediately before the only step that reads them — so they cannot
|
|
# invalidate the dependency layers above.
|
|
ARG APP_VERSION
|
|
ARG GIT_SHA
|
|
ARG BUILD_DATE
|
|
# Vite inlines VITE_*-prefixed env at build time; version falls back to the VERSION file.
|
|
RUN VITE_APP_VERSION="${APP_VERSION:-$(cat ./.version)}" \
|
|
VITE_GIT_SHA="$GIT_SHA" \
|
|
VITE_BUILD_DATE="$BUILD_DATE" \
|
|
npm run build
|
|
|
|
# Stage 2: backend runtime, serving the built SPA
|
|
FROM python:3.12-slim
|
|
ENV PYTHONDONTWRITEBYTECODE=1 \
|
|
PYTHONUNBUFFERED=1 \
|
|
PIP_DISABLE_PIP_VERSION_CHECK=1
|
|
|
|
WORKDIR /app
|
|
|
|
# Create the app user BEFORE the copies so files land already-owned via COPY --chown (a trailing
|
|
# `chown -R /app` would rewrite every file into a duplicate layer). /downloads must be appuser-owned
|
|
# so a fresh named volume mounted there inherits that ownership (Docker copies the image dir's
|
|
# ownership into a new empty volume) — the worker/API can then write without a manual chown. A bind
|
|
# mount to a host path instead needs that path writable by this uid (see docs/self-hosting.md).
|
|
RUN adduser --disabled-password --gecos "" appuser \
|
|
&& mkdir -p /downloads \
|
|
&& chown appuser /app /downloads
|
|
|
|
# ffmpeg: merge separate video+audio streams + postprocessing, and the Plex VAAPI transcode (E6 S1).
|
|
# intel-media-va-driver (iHD): the VA driver ffmpeg's h264_vaapi needs — libva2 ships in the base but
|
|
# no driver, so without this `vainfo`/`h264_vaapi` fail and transcode silently falls back to libx264.
|
|
# Harmless on hosts with no /dev/dri (the driver just goes unused). Deno: the JavaScript runtime yt-dlp
|
|
# uses to solve YouTube's "n" signature challenge for the web player clients (required alongside the PO
|
|
# token, else only storyboards are offered). yt-dlp auto-detects deno on PATH.
|
|
# `apt-get upgrade` (E32): the base image is a snapshot, and the Debian security fixes released since it
|
|
# was cut are only in the archive — the first Trivy scan found one (pcre2) fixed upstream but not in it.
|
|
ARG DENO_VERSION=v2.9.1
|
|
RUN apt-get update \
|
|
&& apt-get upgrade -y --no-install-recommends \
|
|
&& apt-get install -y --no-install-recommends ffmpeg intel-media-va-driver unzip curl ca-certificates \
|
|
&& curl -fsSL "https://github.com/denoland/deno/releases/download/${DENO_VERSION}/deno-x86_64-unknown-linux-gnu.zip" -o /tmp/deno.zip \
|
|
&& unzip -o /tmp/deno.zip -d /usr/local/bin \
|
|
&& chmod +x /usr/local/bin/deno \
|
|
&& rm /tmp/deno.zip \
|
|
&& rm -rf /var/lib/apt/lists/*
|
|
|
|
# Intel NEO OpenCL runtime (intel-opencl-icd) — needed by the Plex transcode's HDR/10-bit HW path, which
|
|
# tone-maps HDR→SDR on the GPU via `tonemap_opencl` (Gen 9.5 / UHD 630 can't tonemap in the VAAPI VPP, so
|
|
# OpenCL is the only HW route; without this the 10-bit branch drops to CPU libx264). NEO was REMOVED from
|
|
# Debian trixie (this base = python:3.12-slim = trixie), so it's pulled from **bookworm** — the 22.43 there
|
|
# still supports Gen 9.5 and its .deb runs on trixie (verified live on the prod UHD 630). The bookworm apt
|
|
# source is added only for this one install and removed after, so it can't drift the rest of the image.
|
|
# Harmless on hosts with no /dev/dri or no OpenCL use (the runtime just sits unused).
|
|
RUN printf 'Types: deb\nURIs: http://deb.debian.org/debian\nSuites: bookworm\nComponents: main\nSigned-By: /usr/share/keyrings/debian-archive-keyring.pgp\n' > /etc/apt/sources.list.d/bookworm.sources \
|
|
&& apt-get update \
|
|
&& apt-get install -y --no-install-recommends -t bookworm intel-opencl-icd \
|
|
&& rm /etc/apt/sources.list.d/bookworm.sources \
|
|
&& rm -rf /var/lib/apt/lists/*
|
|
|
|
# Install from the PINNED lock (requirements.lock, generated from requirements.txt) so the image is
|
|
# byte-reproducible and the exact versions are the ones pip-audit scanned in CI — not whatever floats
|
|
# up from the floor ranges on build day. Regenerate the lock when requirements.txt changes (see its header).
|
|
# pip itself is removed once it has installed the lock (E32): nothing runs it at runtime, and it carries
|
|
# its own VENDORED copies of urllib3, msgpack and setuptools, which Trivy reports as the image's own —
|
|
# an attack surface and a CVE stream for a tool the running app never calls.
|
|
COPY backend/requirements.lock .
|
|
RUN --mount=type=cache,target=/root/.cache/pip \
|
|
pip install --upgrade pip && pip install -r requirements.lock && pip uninstall -y pip
|
|
|
|
COPY --chown=appuser backend/ .
|
|
# Read at runtime by config.py as the app_version (a committed single source of truth).
|
|
COPY --chown=appuser VERSION ./VERSION
|
|
COPY --chown=appuser --from=frontend /fe/dist ./app/static_spa
|
|
|
|
# Version stamps LAST — keep them below everything expensive (see the header note).
|
|
ARG GIT_SHA
|
|
ARG BUILD_DATE
|
|
ENV GIT_SHA=$GIT_SHA \
|
|
BUILD_DATE=$BUILD_DATE
|
|
|
|
USER appuser
|
|
EXPOSE 8000
|
|
CMD ["sh", "entrypoint.sh"]
|