# syntax=docker/dockerfile:1 # img2threejs — production Dockerfile for a Hugging Face Docker Space. # # Build stages: # 1. fixture — regenerate the test fixture factory through the real # vendored pipeline (strict gate -> explicit unreviewed # hosted-preview manifest -> generation). A broken forge # copy fails here. # 2. nodesmoke — npm ci (three + esbuild), bundle the fixture exactly as # the runtime does, and execute it headlessly in node # (scene-graph smoke: Group, meshes, finite bbox, # sculptRuntime). A non-executing factory fails the build. # 3. runtime — python:3.12-slim, non-root UID 1000, tini init, # uvicorn on 0.0.0.0:7860, /health HEALTHCHECK. # (esbuild is a statically-linked native binary, so the # runtime needs no node; node exists only in stage 2.) # --------------------------------------------------------------------------- FROM python:3.12-slim-bookworm AS fixture WORKDIR /build COPY forge/ forge/ COPY app/__init__.py app/__init__.py COPY app/forge_bridge.py app/forge_bridge.py COPY scripts/build_fixture_factory.py scripts/build_fixture_factory.py COPY tests/fixtures/canned_spec.json tests/fixtures/canned_spec.json RUN python3 scripts/build_fixture_factory.py /build/factory_fixture.ts # --------------------------------------------------------------------------- FROM node:22-bookworm-slim AS nodesmoke WORKDIR /build COPY package.json package-lock.json ./ RUN npm ci --omit=dev --no-audit --no-fund && npm cache clean --force COPY --from=fixture /build/factory_fixture.ts /build/factory.ts COPY app/static/viewer-core.js app/static/viewer-core.js COPY scripts/node_smoke.mjs scripts/node_smoke.mjs RUN set -e; \ factory_export=$(grep -oE 'export function create[A-Za-z0-9]+Model' factory.ts | head -1 | awk '{print $3}'); \ pascal=$(echo "$factory_export" | sed -E 's/^create//; s/Model$//'); \ printf 'export { %s as makeModel, create%sLookDevLights as makeLights } from "./factory.ts";\nexport { mountViewer } from "%s";\n' \ "$factory_export" "$pascal" "/build/app/static/viewer-core.js" > entry.js; \ node_modules/.bin/esbuild entry.js \ --bundle --format=esm --target=es2022 --minify --outfile=model.bundle.js; \ node scripts/node_smoke.mjs /build/model.bundle.js # --------------------------------------------------------------------------- FROM python:3.12-slim-bookworm AS runtime ENV PYTHONUNBUFFERED=1 \ PYTHONDONTWRITEBYTECODE=1 \ PIP_NO_CACHE_DIR=1 \ PIP_DISABLE_PIP_VERSION_CHECK=1 \ HOME=/home/user RUN apt-get update \ && apt-get install -y --no-install-recommends tini curl ca-certificates \ && rm -rf /var/lib/apt/lists/* \ && useradd -m -u 1000 user USER user WORKDIR /home/user/app COPY --chown=user requirements.txt ./ RUN pip install --no-cache-dir -r requirements.txt COPY --chown=user package.json package-lock.json ./ COPY --from=nodesmoke --chown=user /build/node_modules ./node_modules # Runtime source allowlist. Build/test/deploy helpers, fixtures, local # verification evidence and workspace state never enter the final image. COPY --chown=user:user app/ ./app/ COPY --chown=user:user forge/ ./forge/ COPY --chown=user:user LICENSE ./LICENSE EXPOSE 7860 # Local-docker convenience only; the HF runner probes app_port over HTTP. HEALTHCHECK --interval=30s --timeout=5s --start-period=60s --retries=3 \ CMD curl -fsS http://localhost:7860/health || exit 1 ENTRYPOINT ["/usr/bin/tini", "--"] CMD ["python", "-m", "app.main"]