####
# Used for building the LDR service dependencies.
####
FROM python:3.14.9-slim@sha256:b347e2dcbc7f0b6f9fb7182e9da6a187fb10ae715e0692518c1aed0eec94a700 AS builder-base

# Set shell to bash with pipefail for safer pipe handling
SHELL ["/bin/bash", "-o", "pipefail", "-c"]

# Fail the build if the base image's interpreter is older than 3.14.9,
# the minimum production interpreter version. Docker pulls a
# `name:tag@sha256:...` reference by its digest and ignores the tag,
# so this checks the interpreter the digest actually contains. It calls
# sys.exit rather than assert: python -O and PYTHONOPTIMIZE strip assert
# statements, and the check must fail the build in either case.
# See .grype.yaml (CPYTHON).
RUN python3 -c "import sys; v = sys.version.split()[0]; print(f'base image Python {v}'); sys.exit(0 if sys.version_info >= (3, 14, 9) else f'base image Python {v} is older than the minimum production version 3.14.9')"

ARG DEBIAN_FRONTEND=noninteractive

# Fail the build if pyexpat embeds an expat older than 2.9.0, the expat that
# CPython 3.14.9 bundles. The official python images build CPython with its
# bundled expat instead of Debian's libexpat1, so Grype sees libexpat1 but not
# the copy that pyexpat uses. Keep this floor, and the floor of the ldr
# stage's copy of this check, at the bundled expat version of the pinned base
# image. It calls sys.exit rather than assert: python -O and
# PYTHONOPTIMIZE strip assert statements, and the check must fail the build in
# either case. See .grype.yaml (libexpat1).
RUN python3 -c "import pyexpat, sys; v = pyexpat.version_info; print(f'pyexpat uses expat {v}'); sys.exit(0 if v >= (2, 9, 0) else 'pyexpat embeds an expat older than 2.9.0; expected the CPython-bundled expat 2.9.0 or newer')"

# `apt-get upgrade -y` is INTENTIONAL — we want every build to pull the
# latest patched Debian packages so security fixes flow into the image.
# This trades bit-for-bit reproducibility (two rebuilds of the same source
# can produce different layer digests across a Debian patch window) for
# always-fresh-on-CVE behavior. The build-once-promote pipeline mitigates
# the reproducibility loss: prerelease-docker.yml builds once per release
# and the resulting digest is what gets retagged to :1.6.9 / :1.6 / :latest,
# so the released image is bit-identical to the one tested.
# Install system dependencies for SQLCipher and Node.js for frontend build
# Using Acquire::Retries to handle transient Debian mirror errors during CI
RUN apt-get update -o Acquire::Retries=3 && apt-get upgrade -y -o Acquire::Retries=3 \
    && apt-get install -y --no-install-recommends -o Acquire::Retries=3 \
    libsqlcipher-dev \
    sqlcipher \
    libsqlcipher1 \
    build-essential \
    pkg-config \
    curl \
    ca-certificates \
    gnupg \
    # Add NodeSource GPG key and repository directly (pinned to Node.js 24.x LTS)
    # GPG key fingerprint verification for supply chain security
    # Key: NSolid <nsolid-gpg@nodesource.com> (RSA 2048-bit, created 2016-05-23)
    # Fingerprint verified from: https://github.com/nodesource/distributions
    # If key rotates, update NODESOURCE_GPG_FINGERPRINT and verify new key at:
    # https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key
    && NODESOURCE_GPG_FINGERPRINT="6F71F525282841EEDAF851B42F59B5F99B1BE0B4" \
    && curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key -o /tmp/nodesource.gpg.key \
    && ACTUAL_FINGERPRINT=$(gpg --with-fingerprint --with-colons --show-keys /tmp/nodesource.gpg.key 2>/dev/null | grep "^fpr" | head -1 | cut -d: -f10) \
    && if [ "$ACTUAL_FINGERPRINT" != "$NODESOURCE_GPG_FINGERPRINT" ]; then \
         echo "ERROR: NodeSource GPG key fingerprint mismatch!" >&2; \
         echo "Expected: $NODESOURCE_GPG_FINGERPRINT" >&2; \
         echo "Actual:   $ACTUAL_FINGERPRINT" >&2; \
         echo "The NodeSource signing key may have been rotated or compromised." >&2; \
         echo "Verify the new key and update NODESOURCE_GPG_FINGERPRINT if valid." >&2; \
         exit 1; \
       fi \
    && gpg --batch --dearmor -o /usr/share/keyrings/nodesource.gpg /tmp/nodesource.gpg.key \
    && rm /tmp/nodesource.gpg.key \
    && echo "deb [signed-by=/usr/share/keyrings/nodesource.gpg] https://deb.nodesource.com/node_24.x nodistro main" > /etc/apt/sources.list.d/nodesource.list \
    && apt-get update \
    && apt-get install -y --no-install-recommends nodejs \
    && rm -rf /var/lib/apt/lists/*

# Install dependencies and tools (hash-locked for reproducibility +
# OSSF Scorecard Pinned-Dependencies compliance).
# PDM 2.27+ requires installer>=1 and hishel[httpx]>=1.0.0; keep their
# hash-locked pins compatible when updating the bootstrap toolchain.
# Note: wheel>=0.46.2 is required for CVE-2026-24049 fix (path traversal)
# Note: pip 26.2 fixes CVE-2026-13346 (arbitrary file installation through a
# malicious package index). It also retains the CVE-2026-8643,
# CVE-2026-1703, and GHSA-jp4c-xjxw-mgf9 fixes from earlier releases.
#
# Every package is installed with --require-hashes from
# requirements/bootstrap-builder.txt (sha256-pinned per wheel, covering
# cp314 linux amd64+aarch64). This closes the Pinned-Dependencies alerts
# (#8205, #8206) and supersedes the earlier won't-fix stance documented here
# (#7740-#7742): the historical objection was that hash-locking pip itself
# broke rebuilds on mirror/wheel-tag drift — with a fully enumerated,
# multi-platform hash set a drift now fails the build loudly instead of
# silently resolving something else, which is the desired behavior. Rotate
# via the regeneration recipe in the file header.
COPY requirements/bootstrap-builder.txt /tmp/bootstrap-builder.txt
# Wheels-only (--only-binary=:all:): some pin hash sets also list an sdist,
# and a wheel-tag drift must fail loudly, never silently source-build from
# that sdist hash.
RUN pip3 install --no-cache-dir --require-hashes --only-binary=:all: \
    -r /tmp/bootstrap-builder.txt \
    && rm /tmp/bootstrap-builder.txt

# pip 26.2.2 still vendors urllib3 2.7.0. Refresh it from reviewed wheels,
# including ensurepip's seed wheel, until pip ships urllib3 >=2.8.0 (#7094).
COPY requirements/pip-vendor-refresh.txt /tmp/pip-vendor-refresh.txt
COPY scripts/refresh_pip_urllib3.py /tmp/refresh_pip_urllib3.py
COPY tests/ci/standalone/verify_pip_vendor.py /tmp/verify_pip_vendor.py
RUN python3 -m pip download --no-cache-dir --no-deps --only-binary=:all: \
        --require-hashes -r /tmp/pip-vendor-refresh.txt -d /tmp/pip-vendor-input \
    && python3 /tmp/refresh_pip_urllib3.py /tmp/pip-vendor-input /tmp/pip-vendor-output \
        --install /tmp/verify_pip_vendor.py \
    && rm -rf /tmp/pip-vendor-input /tmp/pip-vendor-output \
        /tmp/pip-vendor-refresh.txt /tmp/refresh_pip_urllib3.py /tmp/verify_pip_vendor.py
# disable update check
ENV PDM_CHECK_UPDATE=false
# Increase PDM request timeout from default 15s to 120s for large packages (numpy, torch)
# This helps prevent httpcore.ReadTimeout errors during CI network congestion
ENV PDM_REQUEST_TIMEOUT=120

# NOTE: `DEPS_HASH` was previously declared as a cache-invalidation arg but
# never referenced in a RUN/COPY, so it had no effect — Docker only honors
# ARG values for cache when they're actually used downstream. Cache
# invalidation on dependency changes happens naturally via `COPY pdm.lock`
# below, since the file's content hash changes when deps change.
WORKDIR /install

# Copy dependency files first (changes rarely)
COPY pyproject.toml pyproject.toml
COPY pdm.lock pdm.lock
COPY LICENSE LICENSE
COPY README.md README.md

# Copy frontend build files
COPY package.json package.json
COPY package-lock.json* package-lock.json
COPY vite.config.js vite.config.js

# Source files last (changes most frequently). Note: with the current layout,
# caching benefit is limited because all RUN commands (npm ci, npm run build,
# pdm install) live in the builder stage which rebuilds when builder-base changes.
# This ordering is still good practice for Dockerfile maintainability.
COPY src/ src

####
# Builds the LDR service dependencies used in production.
####
FROM builder-base AS builder

# Install npm dependencies, build frontend, and install Python dependencies
# PDM will automatically select the correct SQLCipher package based on platform
# Using npm ci for reproducible builds with lockfile integrity verification
# These RUNs are separate for caching
RUN npm ci
RUN npm run build
RUN for i in 1 2 3; do \
      if pdm install --prod --no-editable; then \
        break; \
      else \
        echo "PDM install attempt $i failed, retrying in 15s..."; \
        sleep 15; \
      fi; \
    done

# Fail the build if lxml is not using a patched libxml2. lxml's manylinux
# wheels statically bundle their own libxml2 (>= 2.14), so the document loaders
# never touch the system libxml2 (Trixie's 2.9.14, vulnerable to CVE-2026-6653).
# If a future Python ever outruns lxml's wheel coverage, `pdm install` could
# build lxml from an sdist linked against that system libxml2 — this assertion
# makes such a build fail loudly instead of shipping a vulnerable parser.
# See .grype.yaml (CVE-2026-6653).
RUN /install/.venv/bin/python -c "from lxml import etree; v = etree.LIBXML_VERSION; assert v >= (2, 11, 0), f'lxml is linked against vulnerable libxml2 {v} (the CVE-2026-6653 fix is in 2.11.0); expected a wheel-bundled libxml2'; print(f'lxml uses libxml2 {v} (>= 2.11.0 CVE-2026-6653 fix) OK')"

####
# Container for running tests.
####
FROM builder-base AS ldr-test

# Set shell to bash with pipefail for safer pipe handling
# Note: Explicitly set even though inherited from builder-base for hadolint static analysis
SHELL ["/bin/bash", "-o", "pipefail", "-c"]

# Bind to all interfaces inside the container so `docker run -p` works
# from CI test jobs (UI / accessibility / smoke). The app defaults to
# 127.0.0.1 outside Docker for security; the production `ldr` stage sets
# this same env separately, but the test stage inherits from builder-base
# (a different parent than `ldr`), so we must set it here too.
ENV LDR_WEB_HOST=0.0.0.0

ARG DEBIAN_FRONTEND=noninteractive

# Install additional runtime dependencies for testing tools
# Note: Node.js is already installed from builder-base
# Using Acquire::Retries to handle transient Debian mirror errors during CI
# `apt-get upgrade -y` is INTENTIONAL — see the rationale comment on the
# corresponding upgrade in the builder-base stage (top of file).
RUN apt-get update -o Acquire::Retries=3 && apt-get upgrade -y -o Acquire::Retries=3 \
    && apt-get install -y --no-install-recommends -o Acquire::Retries=3 \
    # jq + perl: required by tests/ci/test_combine_ai_reviews.py, which exercises
    # the AI-review combine helper (it uses jq and perl). Without them those tests
    # silently skipped in this pytest lane, leaving the script with no enforced
    # coverage. perl is normally present via perl-base, but install it explicitly
    # so the test's fail-loud-in-CI tool check can never false-alarm.
    jq \
    perl \
    # git: the checked-out .git dir is available at runtime (bind-mounted over
    # /app, see docker-tests.yml's `-v "$PWD":/app`) but the git binary itself
    # is never installed in this image, so tests/database/test_migration_chain_integrity.py
    # and tests/database/test_upgrade_from_pre_migration_install.py -- which shell
    # out to git to diff against the fork's merge-base -- fail with
    # FileNotFoundError instead of running their real assertions.
    git \
    xauth \
    xvfb \
    # Dependencies for Chromium
    fonts-liberation \
    libasound2 \
    libatk-bridge2.0-0 \
    libatk1.0-0 \
    libatspi2.0-0 \
    libcups2 \
    libdbus-1-3 \
    libdrm2 \
    libgbm1 \
    libgtk-3-0 \
    libnspr4 \
    libnss3 \
    libxcomposite1 \
    libxdamage1 \
    libxfixes3 \
    libxkbcommon0 \
    libxrandr2 \
    xdg-utils \
    && rm -rf /var/lib/apt/lists/*

# Set up Puppeteer environment
ENV PUPPETEER_CACHE_DIR=/app/puppeteer-cache
ENV DOCKER_ENV=true
# Don't skip Chrome download - let Puppeteer download its own Chrome as fallback
# ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true

# Create puppeteer cache directory with proper permissions
RUN mkdir -p /app/puppeteer-cache && chmod -R 755 /app/puppeteer-cache

# Install Playwright with Chromium first (before npm packages)
RUN playwright install --with-deps chromium || echo "Playwright install failed, will use Puppeteer's Chrome"

# Copy test package files and lockfiles for npm ci
COPY tests/api_tests_with_login/package.json tests/api_tests_with_login/package-lock.json /install/tests/api_tests_with_login/
COPY tests/ui_tests/package.json tests/ui_tests/package-lock.json /install/tests/ui_tests/
COPY tests/accessibility_tests/package.json tests/accessibility_tests/package-lock.json /install/tests/accessibility_tests/
COPY tests/accessibility_tests/tooling/ /install/tests/accessibility_tests/tooling/

# Install npm packages - Skip Puppeteer Chrome download since we have Playwright's Chrome
WORKDIR /install/tests/api_tests_with_login
ENV PUPPETEER_SKIP_DOWNLOAD=true
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true
RUN for i in 1 2 3; do if npm ci; then break; else echo "npm ci attempt $i failed, retrying..."; sleep 5; fi; done
WORKDIR /install/tests/ui_tests
RUN for i in 1 2 3; do if npm ci; then break; else echo "npm ci attempt $i failed, retrying..."; sleep 5; fi; done
WORKDIR /install/tests/accessibility_tests
RUN for i in 1 2 3; do if npm ci; then exit 0; fi; echo "npm ci attempt $i failed"; sleep 5; done; exit 1
RUN npm test

# Install Node.js Playwright browsers (version may differ from Python playwright)
RUN npx playwright install chromium

# Create a stable symlink to Chrome for Puppeteer/Lighthouse.
# Use the Playwright JavaScript API (chromium.executablePath()) to resolve the
# exact binary path from the installed Node.js Playwright version, avoiding
# hard-coded revision directories that change across releases.
RUN CHROME_PATH=$(node -e "console.log(require('playwright-core').chromium.executablePath())") && \
    if [ -n "$CHROME_PATH" ] && [ -x "$CHROME_PATH" ]; then \
        echo "Symlinking Chrome from: $CHROME_PATH"; \
        ln -sf "$CHROME_PATH" /usr/local/bin/chrome; \
    else \
        echo "WARNING: No Chrome binary found at $CHROME_PATH"; \
    fi

# Set environment variables for Puppeteer to use Playwright's Chrome
ENV PUPPETEER_SKIP_DOWNLOAD=true
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true
ENV PUPPETEER_EXECUTABLE_PATH=/usr/local/bin/chrome

# Copy test files to /app where they will be run from
RUN mkdir -p /app && cp -r /install/tests /app/

# Ensure Chrome binaries have correct permissions
RUN chmod -R 755 /app/puppeteer-cache

WORKDIR /install

# Copy Vite build artifacts from builder stage so bundled CSS/JS are available.
# styles.css is only loaded via Vite (imported in app.js), so without the dist/
# directory the page renders without layout CSS, causing a11y test failures.
COPY --from=builder /install/src/local_deep_research/web/static/dist/ /install/src/local_deep_research/web/static/dist/

# Install the package using PDM
# PDM will automatically select the correct SQLCipher package based on platform
RUN pdm install --no-editable

# Mirror of the chmod in the `ldr` stage (see comment there). The ldr-test
# stage does its own pdm install instead of COPYing the venv from `builder`,
# so it doesn't inherit that fix and would otherwise trip
# _validate_migrations_permissions on every login during UI tests.
RUN find /install/.venv -type d -path '*/local_deep_research/database/migrations' \
        -exec chmod -R go-w {} +

# Configure path to default to the venv python.
ENV PATH="/install/.venv/bin:$PATH"

# Non-root user available for individual CI steps that need to run
# permission-sensitive tests as a real unprivileged user (e.g. the
# tests/security 'serial' battery -- every os.geteuid() == 0 check in
# that suite short-circuits to a skip under root, so those assertions
# are meaningless unless something in the container actually isn't
# root). Deliberately NOT set as this stage's default USER -- see the
# note below.
RUN groupadd -r ldruser && useradd -r -g ldruser -u 1000 -m -d /home/ldruser ldruser

# Note: Test container runs as root by default because CI workflows mount
# source code volumes that are owned by root, and most of the suite
# (coverage/junit output, npm/playwright caches, etc.) writes into that
# mount. The production container (ldr) runs as non-root user for
# security. Steps that don't need to write to the mount (e.g. the serial
# permission tests above) opt into `--user ldruser` individually instead
# of flipping the whole stage non-root.

####
# Fetches the NLTK data the upload parser needs.
####
FROM builder-base AS nltk-data

# The bounded upload parser child (security/isolated_loader.py) sets
# AUTO_DOWNLOAD_NLTK=false, so unstructured's Word, PowerPoint, .xlsx and
# HTML text classification finds this data in the image or the upload fails.
# The script fetches each archive from one pinned nltk_data commit and
# checks its size and SHA-256 before extracting it; NLTK's own downloader
# would take whatever the mutable gh-pages index lists, unchecked. Keep its
# package list equal to isolated_loader.NLTK_DATA_PACKAGES.
COPY scripts/install_nltk_data.py /tmp/install_nltk_data.py
RUN python3 -I /tmp/install_nltk_data.py /nltk_data \
    && rm /tmp/install_nltk_data.py

####
# Runs the LDR service.
###
FROM python:3.14.9-slim@sha256:b347e2dcbc7f0b6f9fb7182e9da6a187fb10ae715e0692518c1aed0eec94a700 AS ldr

# Set shell to bash with pipefail for safer pipe handling
SHELL ["/bin/bash", "-o", "pipefail", "-c"]

# Same interpreter check as builder-base: this stage starts from the base
# image again, and its interpreter is the one the image ships.
RUN python3 -c "import sys; v = sys.version.split()[0]; print(f'base image Python {v}'); sys.exit(0 if sys.version_info >= (3, 14, 9) else f'base image Python {v} is older than the minimum production version 3.14.9')"

ARG DEBIAN_FRONTEND=noninteractive

# Same pyexpat check as builder-base: this stage starts from the base image
# again, and its interpreter is the one the image ships.
RUN python3 -c "import pyexpat, sys; v = pyexpat.version_info; print(f'pyexpat uses expat {v}'); sys.exit(0 if v >= (2, 9, 0) else 'pyexpat embeds an expat older than 2.9.0; expected the CPython-bundled expat 2.9.0 or newer')"

# Pin pip to 26.2 for the CVE-2026-13346 malicious-index fix and the earlier
# CVE-2026-8643, CVE-2026-1703, and GHSA-jp4c-xjxw-mgf9 fixes.
# Hash-locked (Scorecard Pinned-Dependencies, alert #8207): same rationale as
# the builder-base bootstrap above.
COPY requirements/bootstrap-runtime.txt /tmp/bootstrap-runtime.txt
# Wheels-only (--only-binary=:all:): some pin hash sets also list an sdist,
# and a wheel-tag drift must fail loudly, never silently source-build from
# that sdist hash.
RUN pip3 install --no-cache-dir --require-hashes --only-binary=:all: \
    -r /tmp/bootstrap-runtime.txt \
    && rm /tmp/bootstrap-runtime.txt

# This independent root stage needs the same pip and ensurepip refresh.
COPY requirements/pip-vendor-refresh.txt /tmp/pip-vendor-refresh.txt
COPY scripts/refresh_pip_urllib3.py /tmp/refresh_pip_urllib3.py
COPY tests/ci/standalone/verify_pip_vendor.py /tmp/verify_pip_vendor.py
RUN python3 -m pip download --no-cache-dir --no-deps --only-binary=:all: \
        --require-hashes -r /tmp/pip-vendor-refresh.txt -d /tmp/pip-vendor-input \
    && python3 /tmp/refresh_pip_urllib3.py /tmp/pip-vendor-input /tmp/pip-vendor-output \
        --install /tmp/verify_pip_vendor.py \
    && rm -rf /tmp/pip-vendor-input /tmp/pip-vendor-output \
        /tmp/pip-vendor-refresh.txt /tmp/refresh_pip_urllib3.py /tmp/verify_pip_vendor.py

# Install runtime dependencies for SQLCipher and WeasyPrint.
# `apt-get upgrade -y` is INTENTIONAL — see rationale on the builder-base
# upgrade (top of file). Trade reproducibility for always-fresh CVE patches.
RUN apt-get update && apt-get upgrade -y \
    && apt-get install -y --no-install-recommends \
    sqlcipher \
    libsqlcipher1 \
    # setpriv (from util-linux, already in base image) handles user switching
    # in the entrypoint — no additional package needed
    #
    # WeasyPrint dependencies for PDF generation
    libcairo2 \
    libpango-1.0-0 \
    libpangocairo-1.0-0 \
    libgdk-pixbuf-2.0-0 \
    libffi-dev \
    shared-mime-info \
    # GLib and GObject dependencies (libgobject is included in libglib2.0-0)
    libglib2.0-0 \
    # CJK fonts so WeasyPrint can render Chinese/Japanese/Korean glyphs
    # in exported PDFs — without these the slim base image has no CJK
    # coverage and CJK text vanishes from the output (issue #4055).
    fonts-noto-cjk \
    # Emoji font so emojis in the markdown don't render as "tofu" boxes
    # in the PDF. The base image has no emoji coverage; WeasyPrint picks
    # "Noto Color Emoji" up via the font-family stack in pdf_service.py.
    fonts-noto-color-emoji \
    && rm -rf /var/lib/apt/lists/*

# Create non-root user for running service (security best practice)
RUN groupadd -r ldruser && useradd -r -g ldruser -u 1000 -m -d /home/ldruser ldruser

# Create directories with proper permissions for non-root user
RUN mkdir -p /app/.config/local_deep_research /home/ldruser/.local/share && \
    chown -R ldruser:ldruser /app /home/ldruser && \
    chmod -R 755 /app /home/ldruser

# retrieve packages from build stage
COPY --chown=ldruser:ldruser --from=builder /install/.venv/ /install/.venv
ENV PATH="/install/.venv/bin:$PATH"

# NLTK data for the upload parser (see the nltk-data stage). The directory
# is on NLTK's default search path (nltk.data.path) for every user on
# Linux, so the server and its parser children find it as ldruser without
# NLTK_DATA. It stays root-owned: ldruser can read it but not replace it.
COPY --from=nltk-data /nltk_data/ /usr/local/share/nltk_data/

# Strip world-write bit from the migrations subtree. The runtime check in
# alembic_runner._validate_migrations_permissions refuses to run migrations
# if anything under migrations/versions/ is world-writable, and pip/pdm can
# leave permissive modes on the dir entries depending on the build host's
# umask (see pip#8164, conda#12829). Without this normalisation a per-user
# DB silently stays at its previous Alembic revision on every login, which
# manifests downstream as e.g. "no such table: papers" on academic-source
# saves. Targeted at the migrations subtree only — we deliberately avoid
# blanket-chmoding the venv.
RUN find /install/.venv -type d -path '*/local_deep_research/database/migrations' \
        -exec chmod -R go-w {} +

# Verify SQLCipher as ldruser via setpriv.
# Running as ldruser ensures Python __pycache__ files created during import
# are owned by ldruser. Browser binaries are NOT installed in the production
# image — Playwright is only used for testing (ldr-test stage).
RUN HOME=/home/ldruser setpriv --reuid=ldruser --regid=ldruser --init-groups -- \
    python -c "from local_deep_research.database.sqlcipher_compat import get_sqlcipher_module; \
    sqlcipher = get_sqlcipher_module(); \
    print(f'✓ SQLCipher module loaded successfully: {sqlcipher}')"

# Persistent state. Without VOLUME directives the user loses all research
# data + DBs on `docker rm`. Recommend bind-mounting these in production.
# - /app/.config/local_deep_research: legacy config path (kept for backcompat)
# - /data: where the entrypoint creates logs/, cache/, encrypted_databases/ —
#   the actual user state, see scripts/ldr_entrypoint.sh.
#
# LDR_DATA_DIR pins the application to /data. Without this, the Python
# code falls back to platformdirs.user_data_dir() which resolves to
# /home/ldruser/.local/share/local-deep-research — NOT under any
# declared VOLUME, so a `docker run -v vol:/data ...` user (without
# also setting -e LDR_DATA_DIR=/data) would silently lose all data on
# `docker rm`. Documented run paths (docker-compose.yml, README docker
# run examples) already pass this env var explicitly; setting it here
# makes the VOLUME actually load-bearing for bare `docker run -v ...`
# invocations too.
ENV LDR_DATA_DIR=/data
# Optional Hugging Face model artifacts use the persistent /data volume. Scope
# this override to the Hub artifact cache rather than HF_HOME (which also moves
# credentials and unrelated state). The entrypoint forbids implicit use of any
# ambient Hugging Face token; provider loads additionally pass token=False.
ENV HF_HUB_CACHE=/data/cache/huggingface/hub
VOLUME /app/.config/local_deep_research
VOLUME /data

# NOTE: /scripts/ is image content (ollama entrypoint baked in below), NOT
# user state. Previously declared as VOLUME, but a VOLUME on a directory
# that the image populates causes anonymous-volume creation on every
# `docker run` and silently shadows the script if a user bind-mounts it.
# Removed for correctness.
COPY --chown=ldruser:ldruser scripts/ollama_entrypoint.sh /scripts/ollama_entrypoint.sh

# Copy LDR entrypoint script to handle volume permissions
COPY scripts/ldr_entrypoint.sh /usr/local/bin/ldr_entrypoint.sh

# COPY --chown sets ownership on copied contents, but Docker auto-creates
# parent dirs (/install, /scripts) as root. Fix with non-recursive chown
# (fast — avoids walking 500MB+ of venv files that are already ldruser-owned).
RUN chmod +x /scripts/ollama_entrypoint.sh \
    && chmod +x /usr/local/bin/ldr_entrypoint.sh \
    && chown ldruser:ldruser /install /scripts

EXPOSE 5000

# Bind to all interfaces inside the container so `docker run -p` works.
# (Outside Docker, the app defaults to 127.0.0.1 for security.)
ENV LDR_WEB_HOST=0.0.0.0

# Health check for container orchestration (Docker, Kubernetes, etc.)
# Exec form (CMD [...]) runs python as the direct healthcheck PID, so
# Docker SIGKILLs it directly on --timeout with no ``sh -c`` wrapper to
# leak a reparented child onto PID 1. ``timeout=8`` stays below Docker's
# 10s wall so urlopen raises (python exits 1) cleanly instead of being
# SIGKILLed at 137.
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
    CMD ["python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:5000/api/v1/health', timeout=8)"]

STOPSIGNAL SIGTERM

# Use entrypoint to fix volume permissions, then switch to ldruser
# The entrypoint runs as root to fix /data permissions, then drops to ldruser
ENTRYPOINT ["/usr/local/bin/ldr_entrypoint.sh"]

# Use PDM to run the application (passed to entrypoint as $@)
CMD [ "ldr-web" ]
