# syntax=docker/dockerfile:1
#
# Official corpus-canary image (PRD v2 Item 3). Two variants, one Dockerfile:
#   docker build --target runtime-fat  -t corpus-canary:fat  .   (default -- last stage)
#   docker build --target runtime-slim -t corpus-canary:slim .
#
# fat  (~1.6GB): HHEM-2.1 / MiniLM / DeBERTa-NLI pre-baked -- true offline from `docker run`.
# slim (~430MB): same image, no pre-baked models -- downloaded on first run into
#                whatever ~/.cache/corpus-canary is; mount a volume there to persist.
# (Sizes measured via `docker image inspect --format='{{.Size}}'` on the real
# build, not estimated -- keep this comment in sync with README's Docker table.)
#
# IMPORTANT: do not set ENTRYPOINT to the binary itself (e.g. ["corpus-canary"]).
# The published usage is `docker run <image> corpus-canary scan --help` --
# corpus-canary is passed as part of the command, not invoked implicitly. An
# ENTRYPOINT that already runs the binary would turn that into
# `corpus-canary corpus-canary scan --help`, an invalid subcommand.

# Passed as --build-arg VCS_REF=$(git rev-parse HEAD) for a traceable image;
# defaults to "unknown" so the image still builds standalone without it.
ARG VCS_REF=unknown

########################################
# base -- shared OS setup + a fixed non-root identity. The identity (uid/gid,
# HOME) must be IDENTICAL in every stage that touches the model cache: the
# app's cache_dir defaults resolve via Path.home() with no override, so a
# HOME mismatch between the stage that pre-bakes models and the runtime stage
# would make the pre-baked cache invisible at runtime.
########################################
# Pinned by digest (bookworm codename, not the floating "slim" tag that can
# silently move to a newer Debian release) -- same supply-chain-integrity
# reasoning already applied to the 3 pinned HF model revisions below.
# Trade-off: pinning by digest also freezes Debian's own security patches --
# a CVE fixed upstream in a later bookworm-slim layer will NOT reach this
# image until someone manually re-pins. No automated check exists yet (no CI
# -- see CLAUDE.md SS8). Update procedure until then: periodically pull
# `python:3.12-slim-bookworm` fresh, diff its digest against the one below,
# and re-pin + rebuild by hand; revisit with a Renovate/Dependabot digest-pin
# rule once CI exists.
FROM python:3.12-slim-bookworm@sha256:a116514e19457bcb7af7efe9c3dd0b9b71e85b317694e7882a1c52aa15a78134 AS base
RUN apt-get update && apt-get install --no-install-recommends -y ca-certificates \
    && rm -rf /var/lib/apt/lists/*
RUN groupadd --gid 1000 canary \
    && useradd --uid 1000 --gid canary --create-home --home-dir /home/canary canary
# Pre-created and owned by canary so that a NAMED VOLUME mounted on either
# path (documented in README's Docker section, for the slim variant's
# first-run model cache) inherits these permissions instead of Docker's
# default root:root 0755 for a fresh named volume -- verified for real: an
# unowned mount point here causes a raw "Permission denied" on first write.
# ~/.cache/huggingface is HF_HOME's default (unset here, only overridden in
# runtime-fat) -- where HHEM's trust_remote_code custom modules cache,
# separate from ~/.cache/corpus-canary's model weights (see runtime-fat below).
RUN mkdir -p /home/canary/.cache/corpus-canary /home/canary/.cache/huggingface \
    && chown -R canary:canary /home/canary/.cache
ENV HOME=/home/canary \
    PATH="/opt/venv/bin:${PATH}" \
    PYTHONDONTWRITEBYTECODE=1 \
    PYTHONUNBUFFERED=1

########################################
# builder -- installs corpus-canary into a venv. This stage's size does NOT
# count against the 3GB image budget -- only what later stages COPY from it does.
########################################
FROM base AS builder
USER root
RUN python -m venv /opt/venv
WORKDIR /src
COPY pyproject.toml README.md LICENSE ./
COPY src/ ./src/
# CPU-only torch FIRST, from PyTorch's own CPU wheel index -- installing it
# before "pip install .[extras]" matters: sentence-transformers/transformers
# declare torch with no upper bound or build-variant pin, so pip treats the
# already-installed CPU wheel as satisfying that requirement and never
# reaches for the default PyPI wheel, which on Linux bundles the full CUDA
# runtime (nvidia-*/triton packages) -- measured ~4.6GB in this project's own
# dev venv, which alone would blow the PRD's <=3GB image AC.
RUN /opt/venv/bin/pip install --no-cache-dir --upgrade pip \
    && /opt/venv/bin/pip install --no-cache-dir torch==2.9.1+cpu \
         --index-url https://download.pytorch.org/whl/cpu \
    && /opt/venv/bin/pip install --no-cache-dir ".[pinecone,qdrant,pgvector]"

########################################
# model-fetcher -- pre-bakes the 3 HF models (fat variant only) by exercising
# the REAL wrapper classes rather than hand-rolling huggingface_hub calls:
# this guarantees the cache layout is byte-for-byte what the runtime wrappers
# will look for (same code path, same DEFAULT_MODEL_REVISION pins), instead
# of independently guessing which files each library expects.
########################################
FROM builder AS model-fetcher
USER root
ENV HF_HOME=/opt/hf-cache
RUN mkdir -p /opt/hf-cache && chown -R canary:canary /opt/hf-cache /home/canary
USER canary
RUN /opt/venv/bin/python -c "\
from corpus_canary.models.embeddings import EmbeddingModel; \
from corpus_canary.models.nli import NLIModel; \
from corpus_canary.models.hhem import HHEMModel; \
EmbeddingModel(device='cpu').encode(['warmup']); \
NLIModel(device='cpu').predict([('a', 'b')]); \
HHEMModel(device='cpu').score([('a', 'b')])"

########################################
# runtime-slim -- no pre-baked models.
########################################
FROM base AS runtime-slim
ARG VCS_REF
# Standard OCI labels -- "official image" hygiene. runtime-fat (FROM
# runtime-slim below) inherits these automatically, no need to repeat them.
# No org.opencontainers.image.source: pyproject.toml's own repo URL is still
# a literal "PLACEHOLDER" (no GitHub repo exists yet) -- shipping a fake
# source label in every built image would be worse than omitting it. Add it
# once the real repo URL is known (same TODO as pyproject.toml's own).
LABEL org.opencontainers.image.revision="${VCS_REF}" \
      org.opencontainers.image.vendor="corpus-canary" \
      org.opencontainers.image.title="corpus-canary" \
      org.opencontainers.image.description="Open-source RAG corpus health checker. No API key needed. Runs locally." \
      org.opencontainers.image.licenses="Apache-2.0"
COPY --from=builder /opt/venv /opt/venv
USER canary
WORKDIR /home/canary
ENTRYPOINT []
CMD ["corpus-canary", "--help"]

########################################
# runtime-fat -- pre-baked models, true offline (default / last stage).
# HF_HUB_OFFLINE + TRANSFORMERS_OFFLINE make any accidental network call fail
# fast and loud instead of silently depending on "the cache happens to be warm".
########################################
FROM runtime-slim AS runtime-fat
ENV HF_HOME=/opt/hf-cache \
    HF_HUB_OFFLINE=1 \
    TRANSFORMERS_OFFLINE=1
USER root
COPY --from=model-fetcher --chown=canary:canary /opt/hf-cache /opt/hf-cache
COPY --from=model-fetcher --chown=canary:canary /home/canary/.cache/corpus-canary /home/canary/.cache/corpus-canary
USER canary
