Dockerfile
Rama main · réplica de lectura del repositorio original.
# Release image of the Ferrofin server:
# - the ferrofin-server binary (built from this workspace),
# - ffmpeg/ffprobe (scan probes with ffprobe, transcode needs ffmpeg),
# - jellyfin-web (served at /web).
#
# A plain `docker build .` is fully self-contained: the `web-build` and
# `runtime-build` stages below build jellyfin-web and the ffmpeg runtime from
# public sources. CI keeps its fast path by overriding WEB_IMAGE/RUNTIME_IMAGE
# with prebuilt base images (ci/web.Dockerfile, ci/runtime.Dockerfile — same
# contents, baked once); BuildKit then skips the unused local stages entirely.
# NOTE: kaniko does NOT skip unused stages by default — the CI service-image
# build MUST pass `--skip-unused-stages=true` or it rebuilds jellyfin-web from
# source every release (see .gitlab-ci.yml).
ARG WEB_IMAGE=web-build
ARG RUNTIME_IMAGE=runtime-build
# ── jellyfin-web, built from source (skipped when CI passes WEB_IMAGE) ──
ARG JELLYFIN_WEB_VERSION=12.4
FROM node:24-trixie AS web-source
ARG JELLYFIN_WEB_VERSION
# webpack's production build is memory-hungry; give Node headroom.
ENV NODE_OPTIONS=--max-old-space-size=4096
WORKDIR /web
# jellyfin-web pulls a couple of deps from github.com archive URLs that ECONNRESET;
# raise npm's fetch retries and retry `npm ci` so a transient blip doesn't fail the build.
#
# JELLYFIN_VERSION becomes the bundle's build version (__JF_BUILD_VERSION__), which
# is also the buster of the query cache jellyfin-web saves in the browser: unset, it
# is "Release" forever and a saved cache outlives every upgrade. Jellyfin's own
# image sets it to the web version too, so the cache drops whenever the bundled
# jellyfin-web changes (not on a Ferrofin release that keeps the same web version).
# Keep this in sync with ci/web.Dockerfile.
RUN git clone --depth 1 --branch "v${JELLYFIN_WEB_VERSION}" \
https://github.com/jellyfin/jellyfin-web.git . \
&& npm config set fetch-retries 5 fetch-retry-mintimeout 20000 fetch-retry-maxtimeout 120000 \
&& n=0; until npm ci --no-audit --no-fund; do \
n=$((n+1)); [ "$n" -ge 5 ] && echo "npm ci failed after $n attempts" && exit 1; \
echo "npm ci failed, retry $n/5 after 15s"; sleep 15; \
done \
&& JELLYFIN_VERSION="${JELLYFIN_WEB_VERSION}" npm run build:production
FROM scratch AS web-build
COPY --from=web-source /web/dist /dist
# ── ffmpeg runtime base (skipped when CI passes RUNTIME_IMAGE) ──────────
# jellyfin-ffmpeg over Debian's ffmpeg: SIMD single-pass tonemapping and a
# current libx264 — the difference on 4K HDR transcode start times. It is also
# built --enable-chromaprint, which is what the intro skipper fingerprints
# with; libchromaprint-tools would add an unused fpcalc fallback and Debian's
# separate FFmpeg libraries. Keep this runtime in sync with ci/runtime.Dockerfile.
# curl and gnupg are only needed to import the repository key; purge them and
# their unused dependencies in the installation layer.
FROM debian:trixie-slim AS runtime-build
RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates curl gnupg \
&& curl -fsSL https://repo.jellyfin.org/jellyfin_team.gpg.key \
| gpg --dearmor -o /usr/share/keyrings/jellyfin.gpg \
&& echo "deb [signed-by=/usr/share/keyrings/jellyfin.gpg] https://repo.jellyfin.org/debian trixie main" \
> /etc/apt/sources.list.d/jellyfin.list \
&& apt-get update \
&& apt-get install -y --no-install-recommends jellyfin-ffmpeg8 \
&& ln -s /usr/lib/jellyfin-ffmpeg/ffmpeg /usr/local/bin/ffmpeg \
&& ln -s /usr/lib/jellyfin-ffmpeg/ffprobe /usr/local/bin/ffprobe \
&& apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false curl gnupg \
&& rm -rf /var/lib/apt/lists/*
# ── prebuilt-or-local indirection ───────────────────────────────────────
FROM ${WEB_IMAGE} AS web
# ── server binary ───────────────────────────────────────────────────────
FROM rust:1.99.1-trixie AS build
WORKDIR /src
COPY . .
# .git is excluded from the build context, so bake the release version in from
# the same SERVICE_VERSION build-arg CI already passes (empty for a local build,
# which ferrofin-health's build.rs treats as unset → crate version fallback).
ARG SERVICE_VERSION=
ENV FERROFIN_GIT_DESCRIBE=${SERVICE_VERSION}
# Strip the container binary while leaving local profiling builds unchanged.
RUN CARGO_PROFILE_RELEASE_STRIP=symbols cargo build --locked --release -p ferrofin-server
# ── runtime ─────────────────────────────────────────────────────────────
FROM ${RUNTIME_IMAGE}
COPY --from=build /src/target/release/ferrofin-server /usr/local/bin/ferrofin-server
COPY --from=web /dist /usr/share/ferrofin/web
# The release version, passed from CI (--build-arg SERVICE_VERSION=<tag>). The
# binary reports it (falling back to the crate version when unset for local builds).
ARG SERVICE_VERSION=
ENV SERVICE_VERSION=$SERVICE_VERSION
# FERROFIN_WEB_DIR lives OUTSIDE /data on purpose: /data is a mounted volume that
# would shadow anything baked under it.
ENV FERROFIN_DATA_DIR=/data FERROFIN_BIND_ADDR=0.0.0.0 FERROFIN_PORT=8096 \
FERROFIN_WEB_DIR=/usr/share/ferrofin/web
# Run as a fixed non-root UID (1000, the conventional first-user id media
# containers standardize on). /data is chowned BEFORE the VOLUME declaration so
# anonymous volumes inherit the ownership; bind mounts are the operator's job —
# chown them to 1000:1000 or override with `docker run --user`.
RUN useradd --uid 1000 --user-group --home-dir /data --no-create-home \
--shell /usr/sbin/nologin ferrofin \
&& mkdir -p /data && chown ferrofin:ferrofin /data
USER ferrofin
# The commit the image was built from, for gates/run to check before it measures anything:
# docker build --build-arg SOURCE_COMMIT="$(git rev-parse HEAD)" -t <image> .
ARG SOURCE_COMMIT=
LABEL org.opencontainers.image.revision=$SOURCE_COMMIT
VOLUME /data
EXPOSE 8096 8920 7359/udp
ENTRYPOINT ["ferrofin-server"]