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 |