CanLex / Dockerfile
Beemer
Deploy: the 2026-09-21 code audit and its three verification rounds -- the quantity and relief allowlists, the five Canadian outcome readings, the IRPR s. 17 clock, the retrieval fixes and the performance work (approved_code 82525e1; curated baseline c32bd27 unchanged)
901bf8c
Raw History Blame Contribute Delete
4.19 kB
# CanLex MCP server -- remote (streamable-HTTP) image.
#
# Builds anywhere: Hugging Face Spaces, Google Cloud Run, Fly.io, plain Docker.
# Retrieval is fully local and key-free; the optional CanLII case citator reads
# its key from the CANLII_API_KEY environment variable (supplied as a host
# secret -- the key is never copied into the image).
FROM python:3.12-slim
# libgomp1 is the OpenMP runtime that onnxruntime (the reranker) links against.
RUN apt-get update \
&& apt-get install -y --no-install-recommends libgomp1 \
&& rm -rf /var/lib/apt/lists/*
# Run as a non-root user (UID 1000) -- required by Hugging Face Spaces.
RUN useradd --create-home --home-dir /app --uid 1000 app
WORKDIR /app
# Python dependencies first, so this layer caches across code changes. The
# image installs requirements.lock: the whole set, transitive dependencies
# included, each pinned with its sha256 hashes -- `uv pip compile` of
# requirements.txt for Linux / Python 3.12 (RUNBOOK.md, "Audit follow-up
# implementation (2026-09-21): group C"). Hash-checking mode refuses any
# distribution but the locked one, so every build installs the same set (until
# the 2026-09-21 audit only the 8 direct pins were fixed and starlette, anyio
# and the other transitives were re-resolved at each build).
COPY requirements.lock .
RUN pip install --no-cache-dir --require-hashes -r requirements.lock
# Application code.
COPY --chown=app:app canlex/ ./canlex/
# Pre-fetch the embedder and cross-encoder models so the cache is baked into the
# image and the first request needs no network. The corpus is NOT re-embedded
# here -- the committed embeddings.npz is used as-is; only the bge query model
# (for embedding live queries) and the reranker are downloaded.
#
# Layer order (the code audit of 2026-09-21, optimization 9): this step reads
# canlex/ and the network only -- Embedder() and Reranker() read nothing under
# data/ at init -- so it runs BEFORE the corpus and curated COPYs below. Docker
# rebuilds every layer after the first one whose input changed; after those
# COPYs, every corpus refresh, curated change and composed release (which
# rewrites embeddings.npz) re-ran this step and re-downloaded the 331 MB of model
# files. Here only a change under canlex/ or to requirements.lock does. (Whether
# the Hugging Face builder keeps layers between builds is outside our control.)
# The user and environment it needs come first; the image's final user,
# environment and files are what they were before the move.
USER app
# CANLEX_LOG_DIR turns on the local call log (canlex/calllog.py: one JSON line
# per tool call, arguments included; loganalyze.py mines it). On the Space this
# path is ephemeral -- lost on every restart or rebuild. The durable log is the
# private Hugging Face dataset the server pushes to when the Space secrets
# CANLEX_LOG_DATASET and CANLEX_LOG_TOKEN are set (RUNBOOK.md, "Call log"); the
# HF_HUB_OFFLINE=1 below still refuses every other Hub request, model files
# included -- calllog lets only that dataset's requests through.
ENV HOME=/app \
HF_HOME=/app/.hf_cache \
CANLEX_HTTP=1 \
CANLEX_LOG_DIR=/tmp/canlex-logs \
PORT=7860 \
PYTHONUNBUFFERED=1
RUN python -c "from canlex.embed import Embedder; Embedder()" \
&& python -c "from canlex.rerank import Reranker; Reranker()"
# The processed corpus (section-chunk JSON) and the prebuilt semantic
# embeddings (embeddings.npz, committed via git-LFS). Shipping the embeddings
# as an artifact keeps the build fast: embedding ~20k chunks on the build host
# took ~33 min and was tripping Hugging Face's job timeout as the corpus grew.
# Regenerate with 'python -m canlex.embed' after any corpus change and commit
# the result.
COPY --chown=app:app data/processed/*.json ./data/processed/
COPY --chown=app:app data/processed/embeddings.npz ./data/processed/
# Curated commentary datasets (the canlex_us_disposition tool reads the
# structured source file directly, not just its processed chunks).
COPY --chown=app:app data/curated/ ./data/curated/
# From here on, model files are served from the baked cache, never fetched.
ENV HF_HUB_OFFLINE=1
EXPOSE 7860
CMD ["python", "-m", "canlex.server"]