The Files library starts here. A user's every download, clip and upload now lands in a source folder made per origin (YouTube, Plex clips, uploads, one per other site), decided once in downloads/folders.py at every place a job is made. How long a file stays moves from the shared asset to the user's job: stamped from its folder when it is done, never for a file the user keeps. The GC warns each user once, then takes the expired file out of their library; the cache cap never evicts a kept file, and an evicted one leaves every holder's library. The media mount is /files now (FILES_ROOT, FILES_HOST_PATH, volume siftlode_files); docs/self-hosting.md says how to carry an old volume over. Migration 0102 places every existing job and carries each done file's expiry over from its asset. safe_abs_path takes a missing path and answers None, which drops one baselined type error.
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 and the dev machine the gate runs on 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). /files 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 /files \
|
|
&& chown appuser /app /files
|
|
|
|
# ffmpeg: merge separate video+audio streams + postprocessing, and the Plex VAAPI transcode.
|
|
# 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`: 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 the gate's pip-audit scanned — 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: 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"]
|