commit 4153f62ed4e01c0b3bd52b09c8a9e9ccacc58262
parent 6b7a36a1050a92daf10552158971bb357f7dc2e6
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Tue, 6 Oct 2026 09:30:25 -0400
Merge r18/docker-publish (slice S5: the image runs Node 22 in the build stage and every runtime — the pinned wrangler's floor)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Diffstat:
5 files changed, 74 insertions(+), 9 deletions(-)
diff --git a/Dockerfile b/Dockerfile
@@ -39,10 +39,18 @@
# bookworm is 2.36, trixie 2.41, ubuntu 24.04 2.39 — so building on bookworm and
# running on any of them is safe, and moving NODE_IMAGE to trixie would silently
# break the CUDA target. If you bump one of these, check that direction first.
-ARG NODE_IMAGE=node:20-bookworm-slim
+#
+# NODE'S MAJOR IS THE SECOND RULE, and it must be the same everywhere: the
+# native modules are compiled against the build stage's Node ABI, so every
+# runtime (NODE_IMAGE, RUNTIME_IMAGE here and in docker-compose.vulkan.yml,
+# NODE_MAJOR in runtime-cuda) carries the same major. 22, not 20: the wrangler
+# pinned in common/package.json refuses to start on anything older (its
+# engines floor), and every deploy runs it from this image.
+# common/publish/buildImage.test.ts holds all four to one major and that floor.
+ARG NODE_IMAGE=node:22-bookworm-slim
# The runtime base, overridable per target: docker-compose.vulkan.yml builds with
# trixie because the Vulkan stack needs it (see the parakeet stage below).
-ARG RUNTIME_IMAGE=node:20-bookworm-slim
+ARG RUNTIME_IMAGE=node:22-bookworm-slim
# ubuntu24.04, not 22.04: 22.04 is glibc 2.35, OLDER than the bookworm the
# workspace is built on, and the native modules would not load.
ARG CUDA_DEVEL_IMAGE=nvidia/cuda:12.6.3-devel-ubuntu24.04
@@ -456,7 +464,7 @@ ENV ARCHILYZER_COMMIT=${ARCHILYZER_COMMIT} \
# ---------------------------------------------------------------------------
FROM ${CUDA_RUNTIME_IMAGE} AS runtime-cuda
-ARG NODE_MAJOR=20
+ARG NODE_MAJOR=22
# The same python + pipx + git-filter-repo as runtime-base (read its comments).
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
diff --git a/RUNNING_IN_DOCKER.md b/RUNNING_IN_DOCKER.md
@@ -657,7 +657,7 @@ docker compose down -v # stop AND DELETE the corp
## What is in the image, and what is not
-**Baked in:** node + the installed workspace, the built editor / umtool / homepage,
+**Baked in:** Node 22 (the pinned wrangler needs it) + the installed workspace, the built editor / umtool / homepage,
`yt-dlp`, a JavaScript runtime for it (`deno` — without one yt-dlp warns and
silently loses formats), `ffmpeg`/`ffprobe`, `whisper-cli` (statically linked),
`zip`/`tar`/`xz`/`gzip`, `rsync`, `git`, `curl`, `ps`; `python3` with yt-dlp's
diff --git a/common/publish/buildImage.test.ts b/common/publish/buildImage.test.ts
@@ -126,3 +126,50 @@ test("every runtime target names its own yt-dlp and links the from-source wrappe
assert.equal(links.length, 2);
assert.ok(existsSync(path.join(REPO, "docker", "yt-dlp-from-source.sh")));
});
+
+// Node's major, everywhere the image is built or run: the build stage
+// (NODE_IMAGE), the default runtime (RUNTIME_IMAGE), the Vulkan overlay's
+// runtime and runtime-cuda's nodesource major. The native modules are compiled
+// once against the build stage's ABI, so all of them must agree.
+function imageNodeMajors(): Record<string, number> {
+ const dockerfile = readFileSync(path.join(REPO, "Dockerfile"), "utf8");
+ const vulkan = readFileSync(path.join(REPO, "docker-compose.vulkan.yml"), "utf8");
+ const one = (re: RegExp, text: string, what: string) => {
+ const m = re.exec(text);
+ assert.ok(m, `${what} not found`);
+ return Number(m[1]);
+ };
+ const out: Record<string, number> = {
+ NODE_IMAGE: one(/^ARG NODE_IMAGE=node:(\d+)-/m, dockerfile, "ARG NODE_IMAGE=node:<major>-…"),
+ RUNTIME_IMAGE: one(/^ARG RUNTIME_IMAGE=node:(\d+)-/m, dockerfile, "ARG RUNTIME_IMAGE=node:<major>-…"),
+ NODE_MAJOR: one(/^ARG NODE_MAJOR=(\d+)$/m, dockerfile, "ARG NODE_MAJOR=<major> (runtime-cuda)"),
+ };
+ const vk = [...vulkan.matchAll(/RUNTIME_IMAGE: node:(\d+)-/g)].map((m) => Number(m[1]));
+ assert.ok(vk.length > 0, "docker-compose.vulkan.yml names a RUNTIME_IMAGE");
+ vk.forEach((v, i) => (out[`vulkan RUNTIME_IMAGE #${i + 1}`] = v));
+ return out;
+}
+
+test("every image target runs the Node major the build stage compiled the native modules for", () => {
+ const majors = imageNodeMajors();
+ assert.equal(new Set(Object.values(majors)).size, 1, `one Node major everywhere, found ${JSON.stringify(majors)}`);
+});
+
+// Every deploy runs the wrangler pinned in common's devDependencies from this
+// image, and wrangler refuses to start below its engines floor (4.x: >=22).
+// Skipped where wrangler is not installed.
+test("the image's Node major meets the pinned wrangler's engines floor", (t) => {
+ const pkg = path.join(REPO, "common", "node_modules", "wrangler", "package.json");
+ if (!existsSync(pkg)) {
+ t.skip("wrangler is not installed in common/node_modules");
+ return;
+ }
+ const engines = (JSON.parse(readFileSync(pkg, "utf8")) as { engines?: { node?: string } }).engines?.node ?? "";
+ const floor = /(\d+)/.exec(engines);
+ assert.ok(floor, `wrangler's engines.node (${JSON.stringify(engines)}) names a major`);
+ const major = imageNodeMajors().NODE_IMAGE;
+ assert.ok(
+ major >= Number(floor[1]),
+ `the image runs Node ${major}, and wrangler needs ${engines} — every deploy from the container would exit 1`,
+ );
+});
diff --git a/docker-compose.vulkan.yml b/docker-compose.vulkan.yml
@@ -38,7 +38,7 @@ services:
# The Vulkan stack needs a newer Debian than the default image runs on;
# the Dockerfile explains why, and why the workspace is still BUILT on
# the older one.
- RUNTIME_IMAGE: node:20-trixie-slim
+ RUNTIME_IMAGE: node:22-trixie-slim
image: archilyzer:${ARCHILYZER_TAG:-local}-vulkan
devices:
# The render node. This is the whole GPU passthrough — no toolkit, no
@@ -59,17 +59,17 @@ services:
build:
target: runtime-vulkan
args:
- RUNTIME_IMAGE: node:20-trixie-slim
+ RUNTIME_IMAGE: node:22-trixie-slim
image: archilyzer:${ARCHILYZER_TAG:-local}-vulkan
homepage:
build:
target: runtime-vulkan
args:
- RUNTIME_IMAGE: node:20-trixie-slim
+ RUNTIME_IMAGE: node:22-trixie-slim
image: archilyzer:${ARCHILYZER_TAG:-local}-vulkan
umtool:
build:
target: runtime-vulkan
args:
- RUNTIME_IMAGE: node:20-trixie-slim
+ RUNTIME_IMAGE: node:22-trixie-slim
image: archilyzer:${ARCHILYZER_TAG:-local}-vulkan
diff --git a/plans/release-18.md b/plans/release-18.md
@@ -515,11 +515,21 @@ merged, so the doctor's `wrangler`, `publish-lock` and `index-stamp` checks, the
| `ba83cbff` | `docker-compose.source.yml`: long bind syntax, `create_host_path: false` — a missing host `.git` now fails `up` ("bind source path does not exist: …", verified) instead of becoming an empty root-owned dir |
| `fb1a1f24` | `YTDLP_AUTO_UPDATE`: the doctor reads it with the entrypoint's exact-match rule (`1`, `true`, `yes`, `on` as written; `TRUE` is off in both), +1 test loop; RUNNING_IN_DOCKER.md states the rule |
| `07bc2395` | RUNNING_IN_DOCKER.md: no gitleaks or stagit in the image — the container's source publish skips the secret scan (with its warning) and the history pages; pinned gitleaks a follow-up |
-| this one | the record: only `--target runtime` was built (vulkan and cuda unverified, left for the rollout); the second half's added items; found and left |
+| `e9fe6bdd` | the record: only `--target runtime` was built (vulkan and cuda unverified, left for the rollout); the second half's added items; found and left |
+| `908c7c4a` | **Node 22** (S2's review: the pinned wrangler 4.147.0 has `engines.node >=22.0.0`, so every deploy from a Node 20 image would exit 1): `NODE_IMAGE` and `RUNTIME_IMAGE` `node:22-bookworm-slim` (glibc 2.36, unchanged — the glibc rule holds), the Vulkan overlay `node:22-trixie-slim`, runtime-cuda `NODE_MAJOR=22` on ubuntu 24.04. Two drift tests in `buildImage.test.ts`: one Node major across all of them (the native modules are built once, against the build stage's ABI; proven red with `NODE_MAJOR=20`), and that major ≥ wrangler's `engines.node` floor (skipped here — wrangler is S2's devDependency; S2's worktree has 4.147.0, `>=22.0.0`) |
+| this one | RUNNING_IN_DOCKER.md names Node 22 in the image's contents; this table |
Re-run after the fixes at `07bc2395`: tsc (all workspaces) clean; `doctor`, `buildImage`, `source` and
`envVars` tests **61/61** (`$T/s5-fix-tests.log`).
+Node 22, at `908c7c4a`: `buildImage.test.ts` 5 passed, 1 skipped (the wrangler floor); common tsc clean.
+`--target runtime` rebuilt in the capped builder, exit 0, 349 s; the image is 1.79 GB. Smoke (`-p r18smoke`,
+the same fixture, `down -v` after): `node --version` in the container is **v22.23.3**; the boot log's
+`yt-dlp: /usr/local/bin/yt-dlp 2026.08.19 (image)`; `/api/pulse` 200; the build stage's native modules
+load in the runtime — `lmdb` opens, writes and reads (`process.versions.modules` 127), `msgpackr-extract`'s
+binding loads; `archilyzer doctor` reports `node v22.23.3` ok and every S5 check, exit 1 only for the
+model the smoke skips. vulkan and cuda still unbuilt (above).
+
## Rollout
(Steps 1–7 above; "### As it went" is written as the rollout runs.)