fix(build): give production builds their own container (#93) - #94
Merged
Conversation
…g .next
`docker compose exec spoketowork pnpm build` — the pattern CLAUDE.md teaches
for everything else — failed on a clean checkout of main:
✓ Compiled successfully
unhandledRejection Error: Cannot find module './4873.js'
`next dev` runs inside that container (Dockerfile CMD), so build and dev server
both owned /app/.next. `build` opened by running `clean:next` (rm -rf .next/*),
WATCHPACK_POLLING made the dev server notice instantly and recompile into the
same directory, and its runtime — which resolves chunks flat — overwrote the
production one that had emitted chunks/4873.js. Caught by mtime: the dev
server rewrote webpack-runtime.js six seconds after the build wrote the chunk.
Changes:
- package.json: drop `clean:next` from `build`, delete the script. It was
redundant with `cleanDistDir: true` (which cleans the correct dist dir at the
correct time) and destructive.
- package.json: drop it from `test` too. Note this incidentally fixes a latent
bug — `NODE_OPTIONS='...' pnpm run clean:next && vitest` applied the 4GB heap
to `rm -rf`, not to vitest, so the OOM mitigation from specs 042/043 had
never actually taken effect.
- .husky/pre-commit: stop wiping .next on every commit.
- docker-compose.yml: add a `builder` service — same image, `profiles: ['build']`,
its own `builder_next:/app/.next` volume. The profile is load-bearing: without
it `docker compose up` starts a second dev server, since builder inherits
CMD ["pnpm","run","dev"].
- docker-compose.yml: replace the `.next` tmpfs on `spoketowork` with a named
`next_cache` volume. The tmpfs was inert on any container created before it
was declared, leaving .next on the WSL2 bind mount — which is separately
documented upstream as the cause of spurious "Cannot find module for page"
PageNotFoundErrors, the other error this build produced.
- docker/Dockerfile: pre-create /app/.next node-owned before `USER node`.
Docker initialises a named volume from the image at the mount point; with no
such path in the image the volume is created root-owned and the dev server
crash-loops on "not writable by user node (UID 1000)". Found the hard way.
- next.config.ts: `distDir: process.env.NEXT_DIST_DIR || '.next'` as a
secondary escape hatch for one-off concurrent builds.
Verified: with the dev server serving 200, `docker compose run --rm builder
pnpm build` now exits 0 and exports 68 pages, and the dev server still serves
200 afterwards. That is the exact scenario that failed before.
Refs #93
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three call sites still built via the dev-server container, which is the collision fixed in the previous commit: - scripts/validate-ci.sh: the IN_DOCKER branch ran plain `pnpm build`, and its host branch stopped and restarted the dev server around every build. Now it refuses inside the dev container (naming the correct command) and uses the `builder` service from the host, so the dev server is left alone. - scripts/test-suite.sh: built via $CMD_PREFIX (`docker compose exec -T spoketowork`). Repointed at `builder`. Its "production build corrupts .next permissions - restart container to fix" workaround is now unnecessary and is removed — that was this bug, misread as a permissions quirk. - package.json test:pwa:build: same substitution. `out/` stays on the bind mount, so serving it from the dev container still works. The guard scans /proc rather than using pgrep, which is NOT available in node:22-slim — the ported-verbatim `pgrep -f` version exited "command not found" and the guard silently never fired. A guard that always passes is worse than no guard, so this was caught by testing both directions rather than only the failing one. Verified in both directions, with the detection fed via stdin so the test harness's own argv cannot contain the pattern and self-match (the first version of this test did exactly that and reported a false positive): dev container → DETECTED (refuses); builder container → not detected (builds). Refs #93 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
src/pages/_document.tsx was the only file under src/pages/ — a bare
Html/Head/Main/NextScript document identical to Next's own default, tracked
since the initial commit. docs/TECHNICAL-DEBT.md already recorded that this
project needs no Pages-Router shim ("Tested build without any Pages Router
files - works perfectly. No dummy files or workarounds needed"), and then the
file stayed.
It was the entry in the failing require stack, and `.next/server/pages/
_document.js` is no longer compiled without it. To be precise about what this
does and does not achieve: Next still synthesizes /_app, /_document and
/_error entries in pages-manifest.json regardless — that is unavoidable and
normal. This removes a redundant compiled artifact, not the pages compiler.
Docs, where the wrong pattern was being taught:
- CLAUDE.md: build via `docker compose run --rm builder pnpm build`, with the
`exec` form shown explicitly as wrong. The section previously offered
"docker compose down && docker compose up fixes .next issues", which was
treating the symptom.
- README.md: add the build command to the contributor list, which omitted it.
- specs/044-simplify-next-ride/quickstart.md: dropped a `pnpm run clean:next`
step that would now fail — and that Vitest never needed, since it does not
read .next.
- docs/TECHNICAL-DEBT.md: stop referencing the deleted script.
Refs #93
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #93.
docker compose exec spoketowork pnpm build— the pattern CLAUDE.md teaches for every other command — failed on a clean checkout ofmain:next devruns inside that container, so build and dev server both owned/app/.next.buildopened by runningclean:next(rm -rf .next/*),WATCHPACK_POLLINGmade the dev server recompile instantly, and its runtime — which resolves chunks flat — overwrote the production one. Confirmed by mtime: the dev server rewrotewebpack-runtime.jssix seconds after the build wrotechunks/4873.js.This repo documented the error verbatim in
docs/BUILD-ISSUES.mduntil335beb4deleted it; its remedy was "let CI validate the build." ScriptHammer root-caused it as its #293 and fixed it structurally. This ports that fix.Changes
buildercompose service — same image, its ownbuilder_next:/app/.nextvolume.profiles: ['build']is load-bearing: without itdocker compose upstarts a second dev server, since builder inheritsCMD ["pnpm","run","dev"].next_cachenamed volume replaces the.nexttmpfs onspoketowork. That tmpfs was inert on any container created before it was declared, leaving.nexton the WSL2 bind mount — separately documented upstream as the cause of spuriousCannot find module for pageerrors, the other failure this build produced./app/.nextnode-owned. Docker seeds a named volume from the image at the mount point; with no such path the volume is root-owned and the dev server crash-loops. Must run beforeUSER node.clean:nextdeleted — redundant withcleanDistDir: trueand destructive. Also removed fromtestand from.husky/pre-commit.validate-ci.shrefuses to build inside the dev container;test-suite.shandtest:pwa:buildrepointed atbuilder.test-suite.sh's "production build corrupts .next permissions" restart hack is removed; that was this bug misread as a permissions quirk.src/pages/_document.tsxdeleted — a bareHtml/Head/Main/NextScriptidentical to Next's default.docs/TECHNICAL-DEBT.md:413-419already recorded that no Pages-Router shim is needed here.Regression test
With the dev server serving, in the exact state that failed:
Notes for review
pgrepis not innode:22-slim. The upstream guard uses it, so ported verbatim it exits "command not found" and silently never fires. Replaced with a/proc/*/cmdlinescan, verified in both directions (detects in dev, silent in builder).NODE_OPTIONS='...' pnpm run clean:next && vitestapplied the 4 GB heap torm -rf, not vitest —VAR=x cmd1 && cmd2scopes tocmd1. The OOM mitigation from specs 042/043 had never taken effect. It now does. Suite is green (388 files) but worth watching CI memory._document.tsxdoes not remove the pages compiler — Next synthesizes/_app,/_document,/_errormanifest entries regardless. It removes the compiled_document.jsthat appeared in the crash's require stack.Local: type-check clean, lint 0 errors, 388 test files pass.
🤖 Generated with Claude Code