Spaces:
Sleeping
Sleeping
| # syntax=docker/dockerfile:1.6 | |
| # ββ Stage 1: builder βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Installs all Python dependencies (including native extensions) into a venv | |
| # that is copied to the lean runtime image, keeping the final layer small. | |
| FROM python:3.11-slim AS builder | |
| # Build dependencies required by native extensions (pymupdf, faiss-cpu) | |
| RUN apt-get update && apt-get install -y --no-install-recommends \ | |
| build-essential \ | |
| libgomp1 \ | |
| && rm -rf /var/lib/apt/lists/* | |
| # Quiet pip and prevent root-action hand-wringing in build logs. | |
| ENV PIP_DISABLE_PIP_VERSION_CHECK=1 \ | |
| PIP_ROOT_USER_ACTION=ignore \ | |
| PIP_NO_CACHE_DIR=1 | |
| WORKDIR /build | |
| # Hatchling resolves `readme = "README.md"` and `packages = ["app"]` from | |
| # pyproject.toml during metadata generation, so all three must be present | |
| # *before* `pip install` runs. The build-cache key for this RUN step | |
| # therefore changes whenever pyproject.toml, README.md, or app/ source | |
| # changes β accepted, to keep the install step correct. | |
| COPY pyproject.toml README.md ./ | |
| COPY app/ ./app/ | |
| # Production image installs the runtime project only β NO `[dev]` extras. | |
| # Dev tools (pytest, mypy, black, ruff, httpx) are ~150 MB of code paths | |
| # that production never executes; shipping them widens the attack surface | |
| # and slows cold pulls on free hosting tiers. | |
| # | |
| # Non-editable install builds a wheel and drops it into site-packages, so the | |
| # `.pth` editable link doesn't dangle when the venv is copied into stage 2. | |
| RUN python -m venv /opt/venv && \ | |
| /opt/venv/bin/pip install --upgrade pip && \ | |
| /opt/venv/bin/pip install ".[temporal]" | |
| # ββ Stage 2: production runtime βββββββββββββββββββββββββββββββββββββββββββββββ | |
| FROM python:3.11-slim AS production | |
| RUN apt-get update && apt-get install -y --no-install-recommends \ | |
| libgomp1 \ | |
| curl \ | |
| && rm -rf /var/lib/apt/lists/* | |
| # Hugging Face Spaces requires the container to run as a user with UID 1000. | |
| # Local docker / docker-compose works fine with this UID too, so we standardise | |
| # on it here. Drop `-r` (system flag) β UID 1000 is a regular user, not a | |
| # system account, and combining `-r` with `-u 1000` is contradictory. | |
| RUN groupadd -g 1000 appgroup && \ | |
| useradd -u 1000 -g appgroup -d /home/user -m -s /sbin/nologin appuser | |
| WORKDIR /app | |
| COPY --from=builder /opt/venv /opt/venv | |
| ENV PATH="/opt/venv/bin:$PATH" \ | |
| PYTHONUNBUFFERED=1 \ | |
| PYTHONDONTWRITEBYTECODE=1 \ | |
| HOME=/home/user \ | |
| PIP_DISABLE_PIP_VERSION_CHECK=1 | |
| COPY --chown=appuser:appgroup . . | |
| # Pre-create EVERY writable runtime path the application uses by default, | |
| # including the legacy paths that docker-compose mounts named volumes onto | |
| # (/mnt/data/uploads, /data, /data/faiss_index). When Docker mounts a named | |
| # volume to a path that did NOT exist in the image, the resulting volume is | |
| # owned by root β and the appuser (UID 1000) cannot write to it, so the very | |
| # first POST /upload fails with PermissionError. Pre-creating the dirs as | |
| # appuser-owned makes Docker initialise each new volume with the right | |
| # ownership, fixing the regression. | |
| RUN mkdir -p /home/user/.report_genius/uploads \ | |
| /home/user/.report_genius/faiss_index \ | |
| /home/user/.report_genius/cache \ | |
| /mnt/data/uploads \ | |
| /data/faiss_index \ | |
| /tmp/section_cache && \ | |
| chown -R appuser:appgroup /home/user /mnt/data /data /tmp/section_cache | |
| USER appuser | |
| # Defaults tuned for a public free-tier deployment. Anything here is overridable | |
| # at runtime via the platform's secret/env-var manager. | |
| # | |
| # - DEV_MODE=false: production logging (no SQLAlchemy echo) and triggers the | |
| # strict-secret startup guard in app/main.py. | |
| # - KNOWLEDGE_BASE_ENABLED=false: source PDFs (Behrang RICS Documents, | |
| # RAW Context) are gitignored; the seeder would only log "no files found". | |
| # Skipping it removes a noisy startup task and the app surfaces a WARNING | |
| # to make the trade-off visible in deploy logs. | |
| # - DATABASE/index/upload paths land under $HOME so the container works out | |
| # of the box on Hugging Face Spaces and on local docker without any extra | |
| # mount config. docker-compose overrides them via env vars. | |
| # - PORT=7860 matches the Hugging Face Spaces convention; docker-compose | |
| # pins it to 8000 to preserve existing local URLs. | |
| # - UVICORN_WORKERS=1 is correct for the 1-vCPU HF Spaces free tier; bigger | |
| # hosts can override at runtime to (2 * cores) + 1. | |
| ENV PORT=7860 \ | |
| UVICORN_WORKERS=1 \ | |
| DEV_MODE=false \ | |
| KNOWLEDGE_BASE_ENABLED=false \ | |
| DATABASE_URL=sqlite+aiosqlite:////home/user/.report_genius/dev.db \ | |
| FAISS_INDEX_PATH=/home/user/.report_genius/faiss_index \ | |
| UPLOAD_DIR=/home/user/.report_genius/uploads \ | |
| CACHE_DIR=/home/user/.report_genius/cache | |
| # Documenting both ports β 7860 (HF Spaces default) and 8000 (docker-compose | |
| # override). EXPOSE is informational only; it doesn't bind. | |
| EXPOSE 7860 8000 | |
| # Image-level healthcheck so any orchestrator (HF Spaces, Kubernetes, docker | |
| # Swarm, plain `docker run`) sees readiness without needing a separate | |
| # compose file. start-period=45s gives the lifespan hook time to pre-warm the | |
| # embedding model and FAISS store on cold boot. | |
| HEALTHCHECK --interval=30s --timeout=10s --start-period=45s --retries=3 \ | |
| CMD curl -fsS "http://127.0.0.1:${PORT:-7860}/health" || exit 1 | |
| # JSON exec form with an inner `exec` so uvicorn replaces /bin/sh as PID 1. | |
| # Without this, SIGTERM (sent by Docker / HF Spaces on every redeploy) goes | |
| # to /bin/sh, which doesn't forward it β uvicorn never gets the chance to | |
| # drain in-flight requests and gets SIGKILL after the 10s grace window, | |
| # corrupting FAISS writes and dropping open generations. | |
| CMD ["sh", "-c", "exec uvicorn app.main:app --host 0.0.0.0 --port ${PORT:-7860} --workers ${UVICORN_WORKERS:-1} --loop uvloop --access-log"] | |