Ctrl K
ComunidadGuías…

Dockerfile

Rama dev · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# SPDX-License-Identifier: AGPL-3.0-or-later

# ---- Stage 1: frontend -------------------------------------------------------
# Digest-pinned: the tag documents intent, the digest is what actually builds.
FROM node:24-alpine@sha256:8a32c8cf649a49915a66e81e7a609223c708d0adabf288ee3f4077d765542f85 AS frontend
WORKDIR /app/frontend

# Lockfile first, so a source-only change does not reinstall the dependency tree.
# `npm ci` installs exactly what is locked and fails if package.json disagrees with the
# lockfile -- a plain `npm install` would silently resolve something newer, which is not
# a thing you want happening unattended inside an image build.
COPY frontend/package.json frontend/package-lock.json ./
RUN npm ci

COPY frontend/ ./
# Runs `tsc --noEmit && vite build`, so a type error fails the image build rather than
# shipping a bundle that throws in the browser.
RUN npm run build

# ---- Stage 2: runtime --------------------------------------------------------
FROM ghcr.io/astral-sh/uv:python3.13-bookworm-slim@sha256:531f855bda2c73cd6ef67d56b733b357cea384185b3022bd09f05e002cd144ca AS runtime

ENV PYTHONUNBUFFERED=1 \
    PYTHONDONTWRITEBYTECODE=1 \
    UV_COMPILE_BYTECODE=1 \
    UV_LINK_MODE=copy \
    REAPER_DATA_DIR=/data

WORKDIR /app

# Dependencies first, so a source-only change doesn't reinstall the world.
#
# We install from the committed lockfile (uv.lock), NOT the `>=` floors in pyproject.toml:
# `uv export --frozen` fails if the lock is stale and emits the exact, hash-pinned
# transitive set that CI tested, so two builds weeks apart ship the same tree. For a tool
# that stores full-power Plex credentials and issues delete requests, letting an
# unaudited `>=` resolution land in the image is a supply-chain exposure the lock exists
# to close. `--no-emit-project` excludes reaper itself here -- src/ isn't copied yet, and
# the project is installed editable (with --no-deps) below.
#
# README.md is copied in this same layer because pyproject declares `readme = "README.md"`;
# hatchling's metadata build (the editable install below) raises OSError without it.
COPY pyproject.toml uv.lock README.md ./
RUN uv export --frozen --no-emit-project -o requirements.txt \
    && uv pip install --system --no-cache -r requirements.txt

COPY alembic.ini ./
COPY alembic/ ./alembic/
COPY src/ ./src/
RUN uv pip install --system --no-cache --no-deps -e .

COPY --from=frontend /app/frontend/dist ./frontend/dist

# The app must not run as root: this container holds credentials that can delete a
# media library. But the data folder is a bind mount on most installs, and Docker
# creates a bind mount owned by root -- which a fixed-uid image cannot write. So the
# entrypoint starts as root, chowns /data to PUID:PGID (default 1000), and drops to
# that user with gosu BEFORE anything opens the database. The app process is never
# root, and /app stays root-owned so a compromised process cannot rewrite what it
# executes. gosu comes from Debian's signed repo (like the base image's own apt).
#
# There is no `USER` line on purpose: PID 1 is root only long enough to fix ownership
# and drop. If you would rather it never touch root, pin `user: "1000:1000"` in your
# compose and the entrypoint skips the root branch and runs in place.
RUN apt-get update \
    && apt-get install -y --no-install-recommends gosu \
    && rm -rf /var/lib/apt/lists/* \
    && gosu nobody true \
    && useradd --system --uid 1000 --create-home reaper \
    && mkdir -p /data \
    && chown -R reaper:reaper /data

VOLUME ["/data"]
# The default port; REAPER_PORT below overrides the actual bind.
EXPOSE 8420

HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
    CMD python -c "import os,urllib.request,sys; port=os.environ.get('REAPER_PORT','8420'); sys.exit(0 if urllib.request.urlopen(f'http://127.0.0.1:{port}/api/health', timeout=3).status==200 else 1)"

# Build provenance, shown on the About page and read by the update check. The
# container has no .git (it is dockerignored), so CI passes the short commit it
# already computes; release.yml additionally passes REAPER_RELEASE=1 and the CalVer
# as REAPER_VERSION, which is what the About page then shows. REAPER_UPDATE_REPO
# names the repository whose releases and dev branch the update check follows, so a
# fork's images follow the fork. A local `docker build` with none of these shows
# "dev" and follows the upstream repository. Kept last so a new commit reuses every
# layer above; none of these values is a secret.
ARG REAPER_GIT_SHA=""
ARG REAPER_RELEASE=""
ARG REAPER_VERSION=""
ARG REAPER_UPDATE_REPO=""
ENV REAPER_GIT_SHA=${REAPER_GIT_SHA} \
    REAPER_RELEASE=${REAPER_RELEASE} \
    REAPER_VERSION=${REAPER_VERSION} \
    REAPER_UPDATE_REPO=${REAPER_UPDATE_REPO}

COPY docker-entrypoint.sh /usr/local/bin/
ENTRYPOINT ["docker-entrypoint.sh"]
# REAPER_HOST/REAPER_PORT are honored here (they also shape the recovery link the app
# prints), so .env.example tells the truth about what they do.
#
# --no-proxy-headers is load-bearing, not tidying (rule 101). uvicorn defaults to
# proxy_headers=True with forwarded_allow_ips=127.0.0.1, so on any install where Reaper
# sees its caller as 127.0.0.1 -- host networking, a same-host proxy published to
# 127.0.0.1:8420, another container sharing the netns -- it rewrites scope["client"] and
# scope["scheme"] from X-Forwarded-For/-Proto BEFORE the app exists. That makes the trust
# decision one layer upstream, from a default the operator never set: a caller could then
# rotate a fake address past the per-IP sign-in lockout, and name its own cookie's Secure
# flag. Turning it off leaves reaper.auth.proxy as the only place peer trust is decided,
# which is what its docstring promises and what the trusted-proxies setting is for.
# tests/test_repo_hygiene.py fails if this flag goes missing here or in scripts/dev-local.sh.
CMD ["sh", "-c", "exec uvicorn reaper.main:create_app --factory --no-proxy-headers --host \"${REAPER_HOST:-0.0.0.0}\" --port \"${REAPER_PORT:-8420}\""]