# Stage 1 — build the React SPA. Node 24: package-lock.json is written by # npm 11, which older bundled npms (node 22's 10.x) refuse as out-of-sync. FROM node:24-alpine AS frontend-build WORKDIR /build COPY frontend/package.json frontend/package-lock.json ./ RUN npm ci COPY frontend/ ./ RUN npm run build # Stage 2 — runtime. Every remaining dependency ships a wheel, so there is no # compile step and no toolchain to keep out of the image. The wheel-building # stage that used to sit here existed for quickjs, which compiled from source # and which M2 removed with campaign scripting. FROM python:3.12-slim WORKDIR /app COPY backend/requirements.txt /tmp/requirements.txt RUN pip install --no-cache-dir -r /tmp/requirements.txt && rm /tmp/requirements.txt # Layout mirrors the repo: main.py finds the SPA at ../../frontend/dist # relative to backend/app/main.py. COPY backend/app /app/backend/app COPY --from=frontend-build /build/dist /app/frontend/dist # Database lives on a volume; parent dir is created by the app if missing. ENV AIDND_DB_PATH=/data/data.db VOLUME /data EXPOSE 8000 # Publish this port to loopback only — `-p 127.0.0.1:8000:8000`, which is what # docker-compose.yml does. The listener below is 0.0.0.0 because that is the # only address a published port can reach inside a container; it is not an # invitation to put the storyteller on the LAN, which is single-user and # unauthenticated in local mode. WORKDIR /app/backend # Single worker on purpose: the turn lock and the debug log are in-process # state. # # No --proxy-headers. That existed for a hosted deployment behind a platform # edge, along with the per-IP rate limiting that read X-Forwarded-For. Neither # survives M2, and trusting a forwarded header on a loopback-published port # would be a way to lie to the app rather than a feature. CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]