From 42cf06e4af277d7702127cee54a02ec2e45062c6 Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> Date: Wed, 17 Jun 2026 15:42:55 -0300 Subject: [PATCH] fix(docker): raise build-stage Node heap to stop production-build OOM (#4076) (#4104) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Docker builder stage ran `npm run build` with V8's default heap ceiling (~2 GB). After #4052 forced the heavier webpack engine (Turbopack panics on this Next.js version), the production optimization pass exceeded that ceiling and the build died with "FATAL ERROR: ... JavaScript heap out of memory" at [builder] npm run build. The builder stage now sets NODE_OPTIONS=--max-old-space-size (default 4096 MB, overridable via --build-arg OMNIROUTE_BUILD_MEMORY_MB) before the build; the value propagates to the spawned next build (resolveNextBuildEnv spreads process.env). Build-only — the runtime heap on the runner stage is unchanged, and CI/local builds (which invoke npm run build directly) are unaffected. Regression guard: tests/unit/dockerfile-build-heap-4076.test.ts asserts the builder stage sets the heap ceiling, before npm run build, at >= 4096 MB. --- CHANGELOG.md | 1 + Dockerfile | 11 +++ tests/unit/dockerfile-build-heap-4076.test.ts | 74 +++++++++++++++++++ 3 files changed, 86 insertions(+) create mode 100644 tests/unit/dockerfile-build-heap-4076.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index ea1e968ace..305eff7dc2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ _In development — bullets added per PR; finalized at release._ - **fix(ws): start the LiveWS sidecar with `cwd` at the package root (global/systemd installs)** — the standalone LiveWS launcher (`scripts/start-ws-server.mjs`) re-spawns itself with `node --import tsx ` but did not set `cwd`. When the WebSocket sidecar was launched from outside the package directory — a global npm/homebrew install, or a `systemd`/`launchd` unit started from `$HOME` — Node could not resolve the `tsx` package (`ERR_MODULE_NOT_FOUND: Cannot find package 'tsx'`), and even from the package directory `tsx` could not resolve the tsconfig `@/*` path aliases (e.g. `@/types/databaseSettings`), so the sidecar never booted. The spawn now pins `cwd` to the package root (the directory above `scripts/`, where `package.json` + `tsconfig.json` live), which resolves both `tsx` discovery and the `@/*` aliases regardless of launch directory. ([#4055](https://github.com/diegosouzapw/OmniRoute/issues/4055) — thanks @Rahulsharma0810) - **fix(dashboard): Logs page auto-refresh now works in embedded/proxied dashboards** — the Request Logger gated each auto-refresh tick on a static `document.visibilityState === "visible"` read. Hosts that report a permanent non-`"visible"` state without ever firing a `visibilitychange` event (Docker dashboard wrappers, embedded webviews) froze auto-refresh entirely — only the manual Refresh button worked, a regression from 3.8.24's unconditional polling. The pause is now event-driven and fail-open: polling starts enabled and only pauses after a real `visibilitychange` → hidden transition (still preserving the backgrounded-tab optimization for normal browser tabs). ([#4054](https://github.com/diegosouzapw/OmniRoute/issues/4054) — thanks @tjengbudi) +- **fix(docker): raise the build-stage Node heap to stop the production-build OOM** — the Docker `builder` stage ran `npm run build` with V8's default heap ceiling (~2 GB). After #4052 forced the heavier webpack engine (Turbopack panics on this Next.js version), the production optimization pass exceeded that ceiling and the build died with `FATAL ERROR: … JavaScript heap out of memory` at `[builder] npm run build`. The builder stage now sets `NODE_OPTIONS=--max-old-space-size` (default 4096 MB, overridable via `--build-arg OMNIROUTE_BUILD_MEMORY_MB=…`) before the build; the value propagates to the spawned `next build`. Build-only — the runtime heap (`OMNIROUTE_MEMORY_MB` on the runner stage) is unchanged. ([#4076](https://github.com/diegosouzapw/OmniRoute/issues/4076) — thanks @kamenkadmitry) --- diff --git a/Dockerfile b/Dockerfile index cc82a141dc..2858b379c2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -46,6 +46,17 @@ RUN --mount=type=cache,target=/root/.npm \ # See docs/ops/QUALITY_GATE_PLAYBOOK.md Parte 6. ENV OMNIROUTE_USE_TURBOPACK=0 +# Raise the V8 heap ceiling for the build. The webpack production optimization +# pass (forced above since Turbopack panics) needs more than V8's default ceiling +# (~2 GB) for a codebase this size; a memory-constrained Docker build otherwise +# dies with "FATAL ERROR: ... JavaScript heap out of memory" at `[builder] npm run +# build` (#4076). NODE_OPTIONS propagates to the spawned `next build` child +# (build-next-isolated.mjs → resolveNextBuildEnv spreads process.env). Build-only; +# the runtime heap is set separately on the runner stage (OMNIROUTE_MEMORY_MB). +# Override for hosts with more/less RAM: `--build-arg OMNIROUTE_BUILD_MEMORY_MB=6144`. +ARG OMNIROUTE_BUILD_MEMORY_MB=4096 +ENV NODE_OPTIONS="--max-old-space-size=${OMNIROUTE_BUILD_MEMORY_MB}" + COPY . ./ RUN --mount=type=cache,target=/app/.build/next/cache \ mkdir -p /app/data && npm run build diff --git a/tests/unit/dockerfile-build-heap-4076.test.ts b/tests/unit/dockerfile-build-heap-4076.test.ts new file mode 100644 index 0000000000..2d5b1b9fb3 --- /dev/null +++ b/tests/unit/dockerfile-build-heap-4076.test.ts @@ -0,0 +1,74 @@ +/** + * #4076 — Docker build fails with "JavaScript heap out of memory" during the + * `[builder] npm run build` step. The webpack production optimization pass (forced + * since #4052 replaced the panicking Turbopack engine) needs more heap than V8's + * default ceiling, which a memory-constrained Docker build does not provide. + * + * Fix: the `builder` stage must raise the heap ceiling via NODE_OPTIONS + * (`--max-old-space-size`) BEFORE running `npm run build`, so the value propagates + * to the spawned `next build` child (build-next-isolated.mjs → resolveNextBuildEnv + * spreads process.env). This is a Docker-only change — CI/local builds invoke + * `npm run build` directly and are unaffected. + * + * This guards the mechanism (the heap setting is present and ordered correctly); + * the end-to-end "the OOM is gone" proof is a successful `docker build`. + */ +import test from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); +const dockerfile = fs.readFileSync(path.join(repoRoot, "Dockerfile"), "utf-8"); +const lines = dockerfile.split("\n"); + +/** Line indices that bound the `builder` stage (from its FROM to the next FROM). */ +function builderStageRange(): { start: number; end: number } { + const start = lines.findIndex((l) => /^FROM\s+\S+\s+AS\s+builder\b/i.test(l.trim())); + assert.ok(start >= 0, "Dockerfile must declare a `builder` stage"); + const after = lines.slice(start + 1).findIndex((l) => /^FROM\s+/i.test(l.trim())); + const end = after === -1 ? lines.length : start + 1 + after; + return { start, end }; +} + +test("#4076 builder stage raises the Node heap ceiling via NODE_OPTIONS", () => { + const { start, end } = builderStageRange(); + const stage = lines.slice(start, end); + const heapLineIdx = stage.findIndex( + (l) => /NODE_OPTIONS/.test(l) && /--max-old-space-size/.test(l) + ); + assert.ok( + heapLineIdx >= 0, + "builder stage must set NODE_OPTIONS with --max-old-space-size to avoid the #4076 build OOM" + ); +}); + +test("#4076 the heap ceiling is set BEFORE `npm run build` so it reaches `next build`", () => { + const { start, end } = builderStageRange(); + const stage = lines.slice(start, end); + const heapLineIdx = stage.findIndex( + (l) => /NODE_OPTIONS/.test(l) && /--max-old-space-size/.test(l) + ); + const buildLineIdx = stage.findIndex((l) => /npm run build\b/.test(l)); + assert.ok(buildLineIdx >= 0, "builder stage must run `npm run build`"); + assert.ok(heapLineIdx >= 0, "builder stage must set the heap ceiling"); + assert.ok( + heapLineIdx < buildLineIdx, + "NODE_OPTIONS heap ceiling must be set before the `npm run build` step" + ); +}); + +test("#4076 the build heap default is at least 4096 MB (the V8 default ~2 GB OOMed)", () => { + const { start, end } = builderStageRange(); + const stage = lines.slice(start, end).join("\n"); + // Match the literal default in either `--max-old-space-size=N` or an ARG default + // referenced by the ENV (e.g. ARG OMNIROUTE_BUILD_MEMORY_MB=4096). + const direct = stage.match(/--max-old-space-size=(\d+)/); + const argDefault = stage.match(/ARG\s+\w*MEMORY\w*\s*=\s*(\d+)/i); + const value = Number(direct?.[1] ?? argDefault?.[1] ?? 0); + assert.ok( + value >= 4096, + `build heap default must be >= 4096 MB to clear the #4076 OOM (found ${value})` + ); +});