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 <houminxi@gmail.com>
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-08-09 09:54:34 -03:00
committed by GitHub
parent a39f78c6f8
commit be9f43e1ee
3 changed files with 55 additions and 10 deletions

View File

@@ -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.

View File

@@ -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))

View File

@@ -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=<OMNIROUTE_MEMORY_MB>`.
- 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 |