# syntax=docker/dockerfile:1.7 # --------------------------------------------------------------------------- # BioPatch backend — Hugging Face Spaces + Fly.io compatible. # # IMPORTANT — LLM backend on this image: # Default: JARVIS_LLM_BACKEND=local → llama-cpp-python in-process # Default model: BioMistral-7B Q4_K_M (~4.4 GB weights, ~6 GB RAM with # 8K context). Tuned for the HF Spaces cpu-upgrade tier (8 vCPU / # 32 GB RAM). On the free tier this OOMs at load time — fall back to # a smaller model by overriding JARVIS_LLM_REPO / JARVIS_LLM_FILE / # JARVIS_LLM_CHAT_FORMAT in the Space Variables. # # Why local and not HF Inference on a Space: # HF Spaces sandbox blocks outbound DNS to api-inference.huggingface.co # (anti-loop policy), so the hosted Inference API is unreachable from # inside a Space. Local llama-cpp works. # # Set JARVIS_LLM_BACKEND=hf if you deploy this image OUTSIDE a Space # (e.g. Fly.io) and prefer the hosted API; you'll also need HF_TOKEN. # # Persistent storage: # When the Space has persistent storage enabled, /data is mounted as # a writable volume. The env vars below default to /data paths so the # SQLite DB, wound photos and the GGUF model cache survive restarts. # Without persistent storage, override these to /app paths via the # Space Variables. # # Thread safety: every BLAS / OpenMP / sklearn parallel pool is capped # at 1 thread + the RF model is forced to n_jobs=1 in code. This # eliminates the GGML_ASSERT / 'Failed joining thread' cascade that # crashed the container under simulator load (joblib pool corrupted # llama's GGML workspace). # --------------------------------------------------------------------------- ARG PYTHON_VERSION=3.12 FROM python:${PYTHON_VERSION}-slim AS base ENV PYTHONDONTWRITEBYTECODE=1 \ PYTHONUNBUFFERED=1 \ PIP_NO_CACHE_DIR=1 \ PIP_DISABLE_PIP_VERSION_CHECK=1 \ PIP_PREFER_BINARY=1 RUN apt-get update && apt-get install -y --no-install-recommends \ build-essential \ libopenblas-dev \ ca-certificates \ curl \ && rm -rf /var/lib/apt/lists/* # HF Spaces runs as UID 1000. RUN useradd -m -u 1000 user WORKDIR /app # Core deps first. COPY backend/requirements.txt /app/requirements.txt RUN pip install -r requirements.txt # LLM stack — pull pre-built CPU wheels from abetlen's dedicated index. # llama-cpp-python intentionally publishes only source dists to PyPI; # the binary wheels (built per Python version × platform) live at # https://abetlen.github.io/llama-cpp-python/whl/cpu. With --only-binary # pip refuses to compile from source, so if the wheel is genuinely # missing for our Python it errors out in seconds with a clear message # instead of OOMing 8 minutes into a C++ compile on the HF free tier. COPY backend/requirements-llm.txt /app/requirements-llm.txt RUN pip install --only-binary=:all: \ --extra-index-url https://abetlen.github.io/llama-cpp-python/whl/cpu \ -r requirements-llm.txt COPY backend /app RUN python -m scripts.seed_clinical_data || true # Thread caps — keep sklearn / OpenBLAS / OpenMP / MKL single-threaded. # Combined with the n_jobs=1 forced on the loaded RandomForest model, # this guarantees the joblib threadpool never spawns and never collides # with llama-cpp's GGML workspace. ENV OMP_NUM_THREADS=1 \ OPENBLAS_NUM_THREADS=1 \ MKL_NUM_THREADS=1 \ BLIS_NUM_THREADS=1 \ NUMEXPR_MAX_THREADS=1 \ JARVIS_LLM_BACKEND=local \ JARVIS_LLM_REPO=MaziyarPanahi/BioMistral-7B-GGUF \ JARVIS_LLM_FILE=BioMistral-7B.Q4_K_M.gguf \ JARVIS_LLM_CHAT_FORMAT=mistral-instruct \ JARVIS_LLM_CACHE=/data/models \ JARVIS_LLM_CTX=4096 \ JARVIS_LLM_THREADS=8 \ JARVIS_HF_MODEL=meta-llama/Llama-3.2-3B-Instruct \ DATABASE_URL=sqlite:////data/biopatch.db \ WOUND_PHOTO_DIR=/data/wound_photos \ HF_HOME=/data/.cache/huggingface \ PORT=7860 \ ENABLE_SIMULATOR=true \ SIMULATOR_INTERVAL_SECONDS=15.0 \ AUTO_SEED=true # Pre-create fallback dirs under /app for hosts WITHOUT persistent /data # (Fly.io, local docker run, etc.). On a HF Space with persistent storage # enabled, /data is mounted at runtime and the env vars above override # these paths. RUN mkdir -p /app/ai/artifacts/models /app/data/wound_photos /app/.cache/huggingface \ && chown -R user:user /app # Stay as root for the runtime. HF Spaces persistent /data is mounted # with FUSE-style perms that silently reject chown from inside the # container, so dropping to uid 1000 leaves the volume unwritable # (sqlite OperationalError: unable to open database file). Running # uvicorn as root sidesteps the perm check; the HF sandbox confines # the container regardless. The threading caps + RF n_jobs=1 already # neutralise the previous GGML / joblib collision, so non-root is no # longer required for stability. EXPOSE 8000 7860 CMD ["sh", "-c", "uvicorn main:app --host 0.0.0.0 --port ${PORT:-8000} --workers 1"]