# 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. COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt # Application code, 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 canlex/ ./canlex/ 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/ USER app # CANLEX_LOG_DIR turns on the opt-in query log (query text + outcome only; # loganalyze.py mines it for corpus gaps). On the free tier this path is # ephemeral -- lost on rebuild/sleep -- so treat it as best-effort telemetry; # the durable log is the local server's (see .mcp.json). ENV HOME=/app \ HF_HOME=/app/.hf_cache \ CANLEX_HTTP=1 \ CANLEX_LOG_DIR=/tmp/canlex-logs \ PORT=7860 \ PYTHONUNBUFFERED=1 # 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. RUN python -c "from canlex.embed import Embedder; Embedder()" \ && python -c "from canlex.rerank import Reranker; Reranker()" # 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"]