Moving the site build into a container, the first attempt failed:
vite.config.ts(2,30): error TS2307: Cannot find module 'node:fs'
vite.config.ts(5,14): error TS2304: Cannot find name '__dirname'
vite.config.ts(43,13): error TS2580: Cannot find name 'process'
vite.config.ts has always used Node APIs. tsconfig.json has always had it in
include. And npx tsc --noEmit had always passed.
@types/node was not in package.json and not in the lockfile.
How it had been working
Somebody, at some point, installed something that pulled @types/node in
transitively. It has sat in node_modules ever since, and every typecheck on
that machine resolved against it.
npm ci installs exactly the lockfile. In a fresh container there is no
accumulated node_modules, so the types were not there, and three years of
"this has always worked" ended in eight seconds.
The deploy script ran the same check
This is the part worth dwelling on. The old deploy path did:
( cd site/web && npm ci --silent && npx tsc --noEmit && npm run build )
Same command. It ran npm ci too. And it kept passing — because it ran on the
same laptop, where npm ci prunes to the lockfile but the directory it prunes
had already been the source of truth for what "installed" meant, and the
developer's global state had long since made the question moot.
The check was correct. The environment it ran in was not clean, and had not been clean for long enough that nobody remembered it being otherwise.
What made it visible
Nothing changed about the code. What changed was where it built.
A container build starts from a base image and a lockfile, so it is the first place the question "what does this project actually depend on" gets asked honestly. The answer had been wrong for a long time and nothing had needed to know.
That is the specific value of building in a container, distinct from the usual argument about reproducibility. It is not that the build is more repeatable — it is that the build is interrogated, once, by something with no memory of how the project got to where it is.
The fix
npm install --save-dev @types/node
Declared, locked, and the typecheck passes from clean. One line, and the bug it closes had been latent since before anyone on the project could remember.
What it says about the old pipeline
The tarball deploy shipped source and built it on the box, and this is the class
of failure that arrangement hides. The box had its own long-lived
node_modules; so did the laptop; and both had drifted the same way, so they
agreed.
Two environments agreeing is not the same as either being right. It is often just two copies of the same history.
The pipeline now builds images in CI from a lockfile and ships the artifact. That change was made for other reasons — knowing which build is in production, mainly — and this was the first thing it caught, before it had deployed anything at all.