Co-Study4Grid / Dockerfile
github-actions[bot]
Deploy 7688ef1
13d4e44
Raw
History Blame Contribute Delete
5.68 kB
# 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}"]