From be9f43e1eec7bfcd395d70b1fd69c8e95de3bf15 Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza Date: Sun, 9 Aug 2026 09:54:34 -0300 Subject: [PATCH] cherry-pick(pr-9695): fix(docker): make the webpack build-arg escape hatch actually work (#9872) * build(docker): make the bundler build-arg actually take effect A bare ENV shadows a same-named ARG for the rest of the stage, so --build-arg OMNIROUTE_USE_TURBOPACK=0 was silently ignored and the webpack escape hatch the surrounding comment advertises only ever worked through -e at runtime, never at build time. That mattered because Turbopack compiles in native Rust memory living outside the V8 heap, so OMNIROUTE_BUILD_MEMORY_MB cannot bound it. A build host with a memory ceiling gets SIGKILLed by the cgroup OOM killer with no error text at all, which reads like a hung build rather than an out-of-memory one. * docs(docker): correct the builder stage facts and document its cost The stage table described a builder that no longer exists: it named node:24.15.0-trixie-slim where every stage now derives from node:26-trixie-slim, and said the stage runs `npm run build -- --webpack` where it runs plain `npm run build`, which is Turbopack by default. That second one is worse than stale. A reader who needs the webpack fallback would conclude the Docker build already uses it and never look for the switch. Adds a Build-time resources section covering the two build args, why the V8 heap arg cannot bound Turbopack, and measured ceilings for both bundlers. The runtime paragraphs that followed get their own heading so they no longer read as part of the build-time story. * docs(docker): correct the runtime heap defaults Same drift as the builder stage, in the paragraphs just below it. The image exports OMNIROUTE_MEMORY_MB=1024 and derives NODE_OPTIONS from it, but the guide reported 512 in three places, including the environment variable table. The "if unset, the launcher uses 512" line was misleading in both readings: the image always sets the variable so that branch cannot fire under Docker, and outside Docker the launcher calibrates from host RAM rather than using a flat 512. * docs(changelog): add fragment for #9695 --------- Co-authored-by: Minxi Hou --- Dockerfile | 10 +++- .../fixes/9695-docker-bundler-build-arg.md | 1 + docs/guides/DOCKER_GUIDE.md | 54 +++++++++++++++---- 3 files changed, 55 insertions(+), 10 deletions(-) create mode 100644 changelog.d/fixes/9695-docker-bundler-build-arg.md diff --git a/Dockerfile b/Dockerfile index 905fb294e0..a32305c22d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -93,7 +93,15 @@ RUN --mount=type=cache,id=npm-cache,target=/root/.npm \ # build from 17min to 9min on the same 32-core box. Webpack stays available as the # escape hatch: `--build-arg`/-e OMNIROUTE_USE_TURBOPACK=0. # See docs/ops/QUALITY_GATE_PLAYBOOK.md Parte 6. -ENV OMNIROUTE_USE_TURBOPACK=1 +# +# Declared as ARG+ENV, not a bare ENV: a bare ENV shadows any same-named ARG for +# the rest of the stage, so `--build-arg OMNIROUTE_USE_TURBOPACK=0` was silently +# ignored and the escape hatch above only ever worked via `-e` at runtime, never +# at build time. Turbopack compiles in native Rust memory that lives outside the +# V8 heap, so OMNIROUTE_BUILD_MEMORY_MB cannot bound it and a memory-constrained +# build host gets SIGKILLed by the cgroup OOM killer with no error message. +ARG OMNIROUTE_USE_TURBOPACK=1 +ENV OMNIROUTE_USE_TURBOPACK="${OMNIROUTE_USE_TURBOPACK}" # Next.js basePath is fixed at build time; pass OMNIROUTE_BASE_PATH here when the # image should serve under a reverse-proxy subpath without a runtime patch. diff --git a/changelog.d/fixes/9695-docker-bundler-build-arg.md b/changelog.d/fixes/9695-docker-bundler-build-arg.md new file mode 100644 index 0000000000..3bc3f0ee7d --- /dev/null +++ b/changelog.d/fixes/9695-docker-bundler-build-arg.md @@ -0,0 +1 @@ +- **fix(docker):** `--build-arg OMNIROUTE_USE_TURBOPACK=0` now reaches the builder stage — a bare `ENV` was shadowing the `ARG`, so the documented webpack escape hatch was silently ignored and memory-constrained hosts were OOM-killed with no error output ([#9695](https://github.com/diegosouzapw/OmniRoute/pull/9695)) diff --git a/docs/guides/DOCKER_GUIDE.md b/docs/guides/DOCKER_GUIDE.md index 5233889912..3180c10e3b 100644 --- a/docs/guides/DOCKER_GUIDE.md +++ b/docs/guides/DOCKER_GUIDE.md @@ -147,11 +147,11 @@ The prod stack runs in parallel with the dev compose (different container names, The repository ships a multi-stage Dockerfile (`Dockerfile`). Three stages are exposed; pick the right `target` for your use case. -| Stage | Base image | Purpose | -| ------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `builder` | `node:24.15.0-trixie-slim` | Installs deps (`npm ci --legacy-peer-deps`) and runs `npm run build -- --webpack` | -| `runner-base` | `node:24.15.0-trixie-slim` | Production runtime with the Next.js standalone output. **No provider CLIs bundled.** | -| `runner-cli` | `runner-base` | Adds `git`, `docker.io`, `docker-compose` and global CLIs: `@openai/codex`, `@anthropic-ai/claude-code`, `droid`, `openclaw`. **Pick this for agentic workflows.** | +| Stage | Base image | Purpose | +| ------------- | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `builder` | `node:26-trixie-slim` | Installs deps (`npm ci --legacy-peer-deps`) and runs `npm run build` (Turbopack by default — see Build-time resources below) | +| `runner-base` | `node:26-trixie-slim` | Production runtime with the Next.js standalone output. **No provider CLIs bundled.** | +| `runner-cli` | `runner-base` | Adds `git`, `docker.io`, `docker-compose` and global CLIs: `@openai/codex`, `@anthropic-ai/claude-code`, `droid`, `openclaw`. **Pick this for agentic workflows.** | Build a specific target manually: @@ -160,14 +160,50 @@ docker build --target runner-base -t omniroute:base . docker build --target runner-cli -t omniroute:cli . ``` -Defaults exported by `runner-base`: `PORT=20128`, `HOSTNAME=0.0.0.0`, `NODE_OPTIONS=--max-old-space-size=512`, `DATA_DIR=/app/data`, `OMNIROUTE_MIGRATIONS_DIR=/app/migrations`. +### Build-time resources + +Two build args control what the `builder` stage costs. They are build-time only — +`OMNIROUTE_MEMORY_MB` (below) is a separate, runtime knob. + +| Build arg | Default | Effect | +| --------------------------- | ------- | ---------------------------------------------------------------------- | +| `OMNIROUTE_USE_TURBOPACK` | `1` | `0` builds with webpack instead. Lower peak memory, slower. | +| `OMNIROUTE_BUILD_MEMORY_MB` | `4096` | V8 heap ceiling (`--max-old-space-size`) for the spawned `next build`. | + +Turbopack compiles in native Rust memory that lives **outside** the V8 heap, so +`OMNIROUTE_BUILD_MEMORY_MB` does not bound it. On a host with a memory ceiling the +build is then SIGKILLed by the OOM killer with no error text at all — it simply +stops mid-`Creating an optimized production build`, which reads like a hang rather +than an out-of-memory. If the build host is constrained, switch bundlers: + +```bash +docker build --target runner-base \ + --build-arg OMNIROUTE_USE_TURBOPACK=0 \ + -t omniroute:base . +``` + +`webpackBuildWorker` is enabled, so `next build` runs a parent **and** a worker +process and each honours `OMNIROUTE_BUILD_MEMORY_MB` separately. Size the container +ceiling above roughly twice that value, not once. + +Measured on this tree (`--target runner-base`, `OMNIROUTE_BUILD_MEMORY_MB=6144`): + +| Bundler | Container ceiling | Result | +| --------- | ----------------- | ----------------------------- | +| Turbopack | 8 GiB / 16 GiB | OOM-killed at both, silently | +| webpack | 8 GiB | build worker SIGKILLed | +| webpack | 12 GiB | succeeded, peaked at 11.1 GiB | + +### Runtime defaults + +Defaults exported by `runner-base`: `PORT=20128`, `HOSTNAME=0.0.0.0`, `OMNIROUTE_MEMORY_MB=1024`, `NODE_OPTIONS=--max-old-space-size=1024`, `DATA_DIR=/app/data`, `OMNIROUTE_MIGRATIONS_DIR=/app/migrations`. Memory behavior in Docker: -- `NODE_OPTIONS=--max-old-space-size=512` is baked into the image as a fallback. +- The image sets `OMNIROUTE_MEMORY_MB=1024` and derives `NODE_OPTIONS=--max-old-space-size=1024` from it. - The actual server process is started by the standalone launcher, which reads `OMNIROUTE_MEMORY_MB` and appends `--max-old-space-size=`. - Node uses the last repeated `--max-old-space-size` value, so setting `OMNIROUTE_MEMORY_MB` controls the effective Docker heap limit. -- If `OMNIROUTE_MEMORY_MB` is unset, the launcher uses `512`. +- Because the image always sets it, the launcher's own RAM-calibrated fallback never applies under Docker. Raise it explicitly (`-e OMNIROUTE_MEMORY_MB=2048`) on a host with headroom. ## Critical Environment Variables @@ -180,7 +216,7 @@ Beyond the defaults documented in [ENVIRONMENT.md](../reference/ENVIRONMENT.md), | `REDIS_PORT` | Host-side port for the bundled Redis container | `6379` | | `REDIS_BIND_HOST` | Host interface the bundled Redis port is published on (loopback unless you add AUTH) | `127.0.0.1` | | `AUTO_UPDATE_HOST_REPO_DIR` | Host path mounted into `cli` profile at `/workspace/omniroute` for self-update workflows | `.` (current directory) | -| `OMNIROUTE_MEMORY_MB` | Runtime Node heap ceiling for the Docker standalone server; overrides the image fallback above | `512` | +| `OMNIROUTE_MEMORY_MB` | Runtime Node heap ceiling for the Docker standalone server; overrides the image default above | `1024` | | `DASHBOARD_PORT` / `API_PORT` | Override exposed ports for dashboard (20128) and API (20129) | `20128` / `20129` | | `OMNIROUTE_BASE_PATH` | URL subpath when the app is published behind a reverse proxy (e.g. `/omniroute`) | _(empty = root)_ | | `NEXT_PUBLIC_BASE_URL` | Public browser origin including the subpath (e.g. `https://host/omniroute`) | unset |