churn-api / Dockerfile
BrejBala's picture
fix: logs/ permissions + bake telco CSV for /explain SHAP
7990267
Raw
History Blame Contribute Delete
4.64 kB
# ── 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"]