Spaces:
Sleeping
Sleeping
| # Co-Study4Grid — single-container image for a HuggingFace Docker Space. | |
| # | |
| # The frontend (built same-origin) and the FastAPI backend are served by one | |
| # uvicorn process on port 7860. Bundled sample grids (data/) let the Game Mode | |
| # presets resolve out of the box. See deploy/huggingface/ for the Space README | |
| # and step-by-step setup. | |
| # | |
| # Build context = repo root: docker build -t costudy4grid . | |
| # --------------------------------------------------------------------------- | |
| # Stage 1 — build the React SPA (same-origin API, game mode on by default). | |
| # --------------------------------------------------------------------------- | |
| FROM node:20-bookworm-slim AS frontend | |
| WORKDIR /build | |
| COPY frontend/package.json frontend/package-lock.json ./ | |
| RUN npm ci | |
| COPY frontend/ ./ | |
| # Empty base URL → relative `/api/...` requests, served by the backend below. | |
| # VITE_GAME_MODE=1 → the Space boots straight into the timed game shell. | |
| ENV VITE_API_BASE_URL="" \ | |
| VITE_GAME_MODE="1" | |
| RUN npm run build | |
| # --------------------------------------------------------------------------- | |
| # Stage 2 — Python runtime serving API + built SPA + bundled grids. | |
| # --------------------------------------------------------------------------- | |
| FROM python:3.11-slim-bookworm AS runtime | |
| # graphviz `dot`: overflow-graph rendering. libgomp1: OpenMP runtime that the | |
| # scientific wheels (numpy / scipy / lightsim2grid) link against. | |
| RUN apt-get update && apt-get install -y --no-install-recommends \ | |
| graphviz \ | |
| libgomp1 \ | |
| && rm -rf /var/lib/apt/lists/* | |
| # HuggingFace Spaces run the container as uid 1000 ("user"). Keep the app under | |
| # its home so runtime writes (Overflow_Graph/, config.json, session folders) | |
| # land on a writable path. | |
| RUN useradd -m -u 1000 user | |
| USER user | |
| ENV HOME=/home/user \ | |
| PATH=/home/user/.local/bin:$PATH \ | |
| PYTHONUNBUFFERED=1 | |
| WORKDIR /home/user/app | |
| # --- Python dependencies (own layer for caching) --------------------------- | |
| # `pip install .` pulls the declared runtime deps (ExpertOp4Grid, pypowsybl, | |
| # fastapi, …). The recommender ships with `--no-deps` — mirroring CI — because | |
| # its own dependency tree is self-conflicting (it wants numpy>=2 while its | |
| # transitive `pypowsybl2grid` pins numpy==1.26.4); the working runtime deps | |
| # come from `pip install .`. It is pinned via recommender-pin.txt (QW8) — the | |
| # SAME file CI installs — so a zero-change rebuild can't drift to a new | |
| # release. overrides.txt forces the pinned transitive versions last. | |
| COPY --chown=user pyproject.toml README.md overrides.txt recommender-pin.txt ./ | |
| COPY --chown=user expert_backend/ ./expert_backend/ | |
| RUN pip install --no-cache-dir --upgrade pip \ | |
| && pip install --no-cache-dir . \ | |
| && pip install --no-cache-dir --no-deps -r recommender-pin.txt \ | |
| && pip install --no-cache-dir -r overrides.txt | |
| # --- Application code, bundled grids, built SPA ---------------------------- | |
| # Seed the default user config (recommender params + the fr225_400 paths); | |
| # the backend copies it to config.json on first boot. | |
| COPY --chown=user config.default.json ./ | |
| COPY --chown=user data/ ./data/ | |
| COPY --chown=user scripts/ ./scripts/ | |
| # The European grid ships compressed (its raw .xiidm exceeds HuggingFace's | |
| # 10 MiB git file limit, so it travels as a Git-LFS .zip). Validate + decompress | |
| # it so pypowsybl can load network.xiidm directly — the "Medium" game difficulty. | |
| # The helper FAILS LOUDLY on an un-smudged LFS pointer or a corrupt archive | |
| # (QW25) instead of silently baking a broken grid into the image. | |
| RUN python scripts/extract_network_zip.py data/pypsa_eur_eur220_225_380_400/network.xiidm.zip | |
| COPY --chown=user --from=frontend /build/dist ./frontend/dist | |
| # The overflow-viewer overlay (services/overflow_overlay.py) inlines this | |
| # shared pin-glyph source module at request time, so it must exist at the | |
| # path it expects even though the rest of frontend/src is not shipped. | |
| COPY --chown=user frontend/src/utils/svg/pinGlyph.js ./frontend/src/utils/svg/pinGlyph.js | |
| # The backend serves this directory at "/" (same origin as the API). | |
| # | |
| # EXPERT_OP4GRID_REASSESSMENT_PARALLEL=0 forces the per-action reassessment to | |
| # run SERIALLY. The Space runs on 2 vCPUs; the recommender's container-aware | |
| # detection already picks serial there, but pinning it makes the guarantee | |
| # explicit and independent of the host's cgroup exposure — parallel worker | |
| # threads each clone a full pypowsybl network, so on 2 vCPUs they over-subscribe | |
| # the CPU and are far SLOWER than serial (the 47 s → ~15 s assessment win). | |
| ENV COSTUDY4GRID_FRONTEND_DIST=/home/user/app/frontend/dist \ | |
| EXPERT_OP4GRID_REASSESSMENT_PARALLEL=0 \ | |
| PORT=7860 \ | |
| COSTUDY4GRID_LOCKDOWN=1 | |
| # COSTUDY4GRID_LOCKDOWN=1 (D7): this is a public, anonymous-visitor | |
| # deployment, so the desktop-era filesystem RPCs (custom config path, | |
| # session save/list/load, native file picker) are disabled — see the | |
| # lockdown profile in expert_backend/main.py. | |
| EXPOSE 7860 | |
| # Liveness probe (QW25): the read-only app-config endpoint stays available even | |
| # under lockdown (D7), so it's a safe heartbeat. The long start-period covers | |
| # the slow first network load. Uses $PORT so it tracks a non-default port. | |
| HEALTHCHECK --interval=30s --timeout=5s --start-period=120s --retries=3 \ | |
| CMD python -c "import os,sys,urllib.request; urllib.request.urlopen('http://127.0.0.1:%s/api/user-config' % os.environ.get('PORT','7860')).read(); sys.exit(0)" | |
| # Shell form so ${PORT} expands (QW25: PORT was decorative before); `exec` | |
| # keeps uvicorn as PID 1 so it receives SIGTERM directly for clean shutdown. | |
| CMD ["sh", "-c", "exec uvicorn expert_backend.main:app --host 0.0.0.0 --port ${PORT:-7860}"] | |