# ── Stage 1: builder ───────────────────────────────────────────────────────── # Installs all Python deps into a venv that is copied to the slim runtime stage. FROM python:3.11-slim AS builder ENV PYTHONDONTWRITEBYTECODE=1 \ PYTHONUNBUFFERED=1 WORKDIR /build # uv is only needed in the builder to export a pip-compatible requirements file. RUN pip install --no-cache-dir uv # Copy dependency manifests before source so Docker layer-caches the install # step when only code changes. COPY pyproject.toml uv.lock ./ # Build the venv with a CPU-only torch wheel. # # uv.lock pins the CUDA-enabled torch build (+ all nvidia-*/cuda-*/triton # packages) which adds ~9 GB. We run CPU-only inference, so instead we: # 1. Export deps without hashes (one package per line, easy to grep). # 2. Strip torch and every CUDA/GPU package from the generated requirements. # 3. Install torch from the official CPU wheel index at the SAME version uv # locked — this avoids the ~7 GB of CUDA libraries entirely. # 4. Install everything else from the filtered requirements (pip sees torch # already present at a satisfying version and skips it). RUN uv export --no-dev --frozen --no-hashes -o /tmp/req-full.txt \ && grep -vE '^(torch|nvidia-|cuda-|triton)' /tmp/req-full.txt > /tmp/req.txt \ && TORCH_VER=$(sed -n 's/^torch==\([^ ;]*\).*/\1/p' /tmp/req-full.txt | head -1) \ && python -m venv /opt/venv \ && /opt/venv/bin/pip install --no-cache-dir \ "torch==${TORCH_VER}" \ --index-url https://download.pytorch.org/whl/cpu \ && /opt/venv/bin/pip install --no-cache-dir -r /tmp/req.txt \ # xgboost declares nvidia-nccl-cu12 for distributed GPU training, which is # never used in CPU-only inference. Strip it to keep the image GPU-free. && /opt/venv/bin/pip uninstall -y nvidia-nccl-cu12 2>/dev/null || true # Pre-download the sentence-transformers embedding model used by the RAG layer # so the runtime container needs no outbound network access at startup, and the # non-root churn user never needs to write to the HF cache. RUN HF_HOME=/opt/hf-cache /opt/venv/bin/python -c \ "from sentence_transformers import SentenceTransformer; SentenceTransformer('all-MiniLM-L6-v2')" # ── Stage 2: runtime ───────────────────────────────────────────────────────── FROM python:3.11-slim AS runtime ENV PYTHONDONTWRITEBYTECODE=1 \ PYTHONUNBUFFERED=1 \ # Put the venv on PATH so every python/pip call uses it without activation. PATH="/opt/venv/bin:$PATH" \ # Make churn/ and api/ importable without a package install. PYTHONPATH=/app \ # Point sentence-transformers/HuggingFace Hub at the pre-downloaded cache so # the non-root churn user never needs write access to download the model. HF_HOME=/opt/hf-cache WORKDIR /app # Non-root user — drop privileges before the server starts. RUN groupadd -r churn && useradd -r -g churn -d /app churn # Copy only the venv (no build tools, no uv, no temp files). COPY --from=builder /opt/venv /opt/venv # Copy the pre-downloaded HuggingFace model cache and give the churn user # ownership so sentence-transformers can write lock files on first load. COPY --from=builder /opt/hf-cache /opt/hf-cache RUN chown -R churn:churn /opt/hf-cache # Application code. Copy explicit directories so stray local files (logs/, # mlruns/, .env) never enter the image even if .dockerignore is incomplete. COPY churn/ churn/ COPY api/ api/ COPY data/playbooks/ data/playbooks/ COPY data/raw/telco_churn.csv data/raw/telco_churn.csv # reports/threshold.json is the fallback threshold when the registry tag is absent. COPY reports/ reports/ # Create writable runtime directories before dropping to non-root. # /app is owned by root after WORKDIR; the churn user needs logs/ for the # SQLite prediction log, .streamlit/ is unused here but harmless to create. RUN mkdir -p /app/logs && chown -R churn:churn /app USER churn EXPOSE 7860 # Lightweight health probe — no shell required. HEALTHCHECK --interval=30s --timeout=5s --start-period=60s --retries=3 \ CMD python -c \ "import urllib.request; urllib.request.urlopen('http://localhost:7860/health')" \ || exit 1 # Champion model + MLflow auth come from env at runtime, not baked in. # Set MLFLOW_TRACKING_URI, MLFLOW_TRACKING_USERNAME, MLFLOW_TRACKING_PASSWORD. CMD ["uvicorn", "api.main:app", "--host", "0.0.0.0", "--port", "7860"]