diff --git a/DEPLOY.md b/DEPLOY.md index eaebd01..d360f4f 100644 --- a/DEPLOY.md +++ b/DEPLOY.md @@ -158,6 +158,31 @@ docker compose up -d --build Rebuild the client whenever a `NEXT_PUBLIC_*` value in `client/.env.production` changes — those are compiled into the browser bundle, so restarting the container alone changes nothing. +## Known wrinkle: the client lockfile is Windows-generated + +`client/package-lock.json` is produced on Windows, where npm filters out wasm32-only optional packages +and therefore never records their transitive dependencies (`@emnapi/core`, `@emnapi/runtime`). On Linux +npm *does* want them, so a bare `npm ci` fails with: + +``` +npm error `npm ci` can only install packages when your package.json and package-lock.json ... are in sync. +npm error Missing: @emnapi/runtime@1.11.3 from lock file +``` + +The client Dockerfile works around this by completing the lock inside the image before installing. To fix +it permanently, regenerate the lock **on Linux** once and commit the result: + +```bash +cd client +docker run --rm -v "$PWD:/app" -w /app node:24-alpine npm install --package-lock-only --no-audit --no-fund +``` + +Then drop the `npm install --package-lock-only` line from `client/Dockerfile`, leaving just `npm ci`. + +Note `--omit=optional` is **not** a valid shortcut here: Turbopack resolves `@parcel/watcher`'s native +binary through `optionalDependencies`, so omitting them breaks `next build` with +`No prebuild or local build of @parcel/watcher found`. + ## Persisted state Two named volumes survive rebuilds. Uploaded verification documents live in the first one; losing it diff --git a/client/Dockerfile b/client/Dockerfile index 3d87a40..f085b29 100644 --- a/client/Dockerfile +++ b/client/Dockerfile @@ -5,12 +5,23 @@ # committed .env.production, which `next build` reads because it runs with NODE_ENV=production; change a value # there and rebuild the image. -FROM node:22-alpine AS deps +FROM node:24-alpine AS deps WORKDIR /app COPY package.json package-lock.json ./ -RUN npm ci -FROM node:22-alpine AS build +# package-lock.json is generated on Windows, where npm filters out the wasm32-only optional packages and so +# never records their transitive deps (@emnapi/core, @emnapi/runtime). On Linux npm does want them, and a +# bare `npm ci` dies on the lockfile-sync check. --omit=optional is NOT the fix: Turbopack's @parcel/watcher +# resolves its native binary through optionalDependencies, so omitting them breaks `next build` outright. +# +# So: complete the lock here, on the platform that can actually see those packages, then install from it. +# --package-lock-only reuses every version already pinned in the committed lock and only ADDS the missing +# Linux-side entries, so this stays effectively reproducible rather than a free-for-all `npm install`. +# Drop the first command once the committed lock is generated on Linux (see DEPLOY.md). +RUN npm install --package-lock-only --no-audit --no-fund \ + && npm ci --no-audit --no-fund + +FROM node:24-alpine AS build WORKDIR /app COPY --from=deps /app/node_modules ./node_modules COPY . . @@ -18,7 +29,7 @@ COPY . . ENV NEXT_TELEMETRY_DISABLED=1 RUN npm run build -FROM node:22-alpine AS final +FROM node:24-alpine AS final WORKDIR /app ENV NODE_ENV=production ENV NEXT_TELEMETRY_DISABLED=1