# rfc-app container image — the Cloud Run keystone for per-PR preview # environments (flotilla SPEC §15). NOT used by production: prod still deploys # via the pin-based on-VM gesture (flotilla §8). This image exists so flotilla # `preview up --pr=N` can build a PR's tree and run it as an ephemeral, # scale-to-zero Cloud Run service with a SEEDED SYNTHETIC database and ZERO real # secrets (test-secret env only — flotilla §15 / §3 invariant 1). # # Single container, single port: nginx serves the built SPA on $PORT (Cloud Run # injects it) and reverse-proxies /api/ + /auth/ to a single-process uvicorn on # 127.0.0.1:8000 — mirroring the prod nginx + systemd split # (deploy/nginx/ohm.wiggleverse.org.conf, deploy/systemd/rfc-app.service), so a # preview behaves like prod minus the secrets. Single uvicorn process + single # SQLite file, per §4.2 (never scale workers). # # Build context is the repo root: docker build -t . # ---- stage 1: build the Vite SPA ------------------------------------------- FROM node:20-slim AS web WORKDIR /app/frontend COPY frontend/package.json frontend/package-lock.json ./ RUN npm ci COPY frontend/ ./ # VITE_* values are baked into the bundle at build time (intentionally public — # §8 phase-5 note). VITE_APP_NAME is REQUIRED by vite.config.js (the framework # ships no default — each deployment names itself); previews self-name. Turnstile # uses Cloudflare's always-pass test SITE key so the widget renders + auto-passes. ARG VITE_APP_NAME="RFC App (preview)" ARG VITE_TURNSTILE_SITE_KEY=1x00000000000000000000AA ARG VITE_AMPLITUDE_API_KEY= RUN VITE_APP_NAME="$VITE_APP_NAME" \ VITE_TURNSTILE_SITE_KEY="$VITE_TURNSTILE_SITE_KEY" \ VITE_AMPLITUDE_API_KEY="$VITE_AMPLITUDE_API_KEY" \ npm run build # ---- stage 2: runtime (backend + nginx) ------------------------------------ FROM python:3.12-slim AS runtime RUN apt-get update \ && apt-get install -y --no-install-recommends nginx gettext-base sqlite3 \ && rm -rf /var/lib/apt/lists/* WORKDIR /opt/rfc-app # Backend deps first for layer caching. COPY backend/requirements.txt backend/requirements.txt RUN pip install --no-cache-dir -r backend/requirements.txt COPY backend/ backend/ COPY deploy/preview/ deploy/preview/ # health.py reads VERSION at parents[2] (== /opt/rfc-app/VERSION). COPY VERSION ./ COPY --from=web /app/frontend/dist/ frontend/dist/ RUN chmod +x deploy/preview/entrypoint.sh # Cloud Run injects $PORT (default 8080); the entrypoint renders nginx against # it. The synthetic preview DB lives on the container's ephemeral filesystem and # dies with the instance (zero create/seed/drop lifecycle — §15). ENV PORT=8080 \ DATABASE_PATH=/opt/rfc-app/backend/data/rfc-app.db EXPOSE 8080 ENTRYPOINT ["deploy/preview/entrypoint.sh"]