fix(docker): reduce local build memory pressure

This commit is contained in:
An Dang
2026-07-01 03:58:21 -07:00
committed by GitHub
parent daffe593aa
commit 98254634f1
8 changed files with 111 additions and 30 deletions
+7 -1
View File
@@ -9,6 +9,9 @@
# Build stages use full bookworm; the runtime image is always bookworm-slim.
ARG OPENCLAW_EXTENSIONS=""
ARG OPENCLAW_BUNDLED_PLUGIN_DIR=extensions
ARG OPENCLAW_DOCKER_BUILD_NODE_OPTIONS="--max-old-space-size=8192"
ARG OPENCLAW_DOCKER_BUILD_TSDOWN_MAX_OLD_SPACE_MB=""
ARG OPENCLAW_DOCKER_BUILD_SKIP_DTS=1
ARG OPENCLAW_NODE_BOOKWORM_IMAGE="docker.io/library/node:24-bookworm@sha256:8530f76a96d88820d288761f022e318970dda93d01536919fbc16076b7983e63"
ARG OPENCLAW_NODE_BOOKWORM_SLIM_IMAGE="docker.io/library/node:24-bookworm-slim@sha256:242549cd46785b480c832479a730f4f2a20865d61ea2e404fdb2a5c3d3b73ecf"
ARG OPENCLAW_NODE_BOOKWORM_SLIM_DIGEST="sha256:242549cd46785b480c832479a730f4f2a20865d61ea2e404fdb2a5c3d3b73ecf"
@@ -49,6 +52,9 @@ FROM ${OPENCLAW_BUN_IMAGE} AS bun-binary
FROM ${OPENCLAW_NODE_BOOKWORM_IMAGE} AS build
ARG OPENCLAW_BUNDLED_PLUGIN_DIR
ARG OPENCLAW_EXTENSIONS
ARG OPENCLAW_DOCKER_BUILD_NODE_OPTIONS
ARG OPENCLAW_DOCKER_BUILD_TSDOWN_MAX_OLD_SPACE_MB
ARG OPENCLAW_DOCKER_BUILD_SKIP_DTS
# Copy pinned Bun binary from the official image instead of fetching via curl.
COPY --from=bun-binary /usr/local/bin/bun /usr/local/bin/bun
@@ -119,7 +125,7 @@ RUN pnpm_config_verify_deps_before_run=false pnpm canvas:a2ui:bundle || \
RUN if printf '%s\n' "$OPENCLAW_EXTENSIONS" | tr ',' ' ' | tr ' ' '\n' | grep -qx 'qa-lab'; then \
export OPENCLAW_BUILD_PRIVATE_QA=1 OPENCLAW_ENABLE_PRIVATE_QA_CLI=1; \
fi && \
NODE_OPTIONS=--max-old-space-size=8192 pnpm_config_verify_deps_before_run=false pnpm build:docker
OPENCLAW_RUN_NODE_SKIP_DTS_BUILD="$OPENCLAW_DOCKER_BUILD_SKIP_DTS" OPENCLAW_TSDOWN_MAX_OLD_SPACE_MB="$OPENCLAW_DOCKER_BUILD_TSDOWN_MAX_OLD_SPACE_MB" NODE_OPTIONS="$OPENCLAW_DOCKER_BUILD_NODE_OPTIONS" pnpm_config_verify_deps_before_run=false pnpm build:docker
# Force pnpm for UI build (Bun may fail on ARM/Synology architectures)
ENV OPENCLAW_PREFER_PNPM=1
RUN pnpm_config_verify_deps_before_run=false pnpm ui:build
+31 -19
View File
@@ -161,25 +161,28 @@ and setup-time config writes through `openclaw-gateway` with
The setup script accepts these optional environment variables:
| Variable | Purpose |
| ------------------------------------------ | --------------------------------------------------------------------- |
| `OPENCLAW_IMAGE` | Use a remote image instead of building locally |
| `OPENCLAW_IMAGE_APT_PACKAGES` | Install extra apt packages during build (space-separated) |
| `OPENCLAW_IMAGE_PIP_PACKAGES` | Install extra Python packages during build (space-separated) |
| `OPENCLAW_EXTENSIONS` | Pre-install plugin dependencies at build time (space-separated names) |
| `OPENCLAW_EXTRA_MOUNTS` | Extra host bind mounts (comma-separated `source:target[:opts]`) |
| `OPENCLAW_HOME_VOLUME` | Persist `/home/node` in a named Docker volume |
| `OPENCLAW_SANDBOX` | Opt in to sandbox bootstrap (`1`, `true`, `yes`, `on`) |
| `OPENCLAW_SKIP_ONBOARDING` | Skip the interactive onboarding step (`1`, `true`, `yes`, `on`) |
| `OPENCLAW_DOCKER_SOCKET` | Override Docker socket path |
| `OPENCLAW_DISABLE_BONJOUR` | Disable Bonjour/mDNS advertising (defaults to `1` for Docker) |
| `OPENCLAW_DISABLE_BUNDLED_SOURCE_OVERLAYS` | Disable bundled plugin source bind-mount overlays |
| `OTEL_EXPORTER_OTLP_ENDPOINT` | Shared OTLP/HTTP collector endpoint for OpenTelemetry export |
| `OTEL_EXPORTER_OTLP_*_ENDPOINT` | Signal-specific OTLP endpoints for traces, metrics, or logs |
| `OTEL_EXPORTER_OTLP_PROTOCOL` | OTLP protocol override. Only `http/protobuf` is supported today |
| `OTEL_SERVICE_NAME` | Service name used for OpenTelemetry resources |
| `OTEL_SEMCONV_STABILITY_OPT_IN` | Opt in to latest experimental GenAI semantic attributes |
| `OPENCLAW_OTEL_PRELOADED` | Skip starting a second OpenTelemetry SDK when one is preloaded |
| Variable | Purpose |
| ----------------------------------------------- | --------------------------------------------------------------------- |
| `OPENCLAW_IMAGE` | Use a remote image instead of building locally |
| `OPENCLAW_IMAGE_APT_PACKAGES` | Install extra apt packages during build (space-separated) |
| `OPENCLAW_IMAGE_PIP_PACKAGES` | Install extra Python packages during build (space-separated) |
| `OPENCLAW_EXTENSIONS` | Pre-install plugin dependencies at build time (space-separated names) |
| `OPENCLAW_DOCKER_BUILD_NODE_OPTIONS` | Override the local source-build Node options |
| `OPENCLAW_DOCKER_BUILD_TSDOWN_MAX_OLD_SPACE_MB` | Override the local source-build tsdown heap in MB |
| `OPENCLAW_DOCKER_BUILD_SKIP_DTS` | Skip declaration output during runtime-only local image builds |
| `OPENCLAW_EXTRA_MOUNTS` | Extra host bind mounts (comma-separated `source:target[:opts]`) |
| `OPENCLAW_HOME_VOLUME` | Persist `/home/node` in a named Docker volume |
| `OPENCLAW_SANDBOX` | Opt in to sandbox bootstrap (`1`, `true`, `yes`, `on`) |
| `OPENCLAW_SKIP_ONBOARDING` | Skip the interactive onboarding step (`1`, `true`, `yes`, `on`) |
| `OPENCLAW_DOCKER_SOCKET` | Override Docker socket path |
| `OPENCLAW_DISABLE_BONJOUR` | Disable Bonjour/mDNS advertising (defaults to `1` for Docker) |
| `OPENCLAW_DISABLE_BUNDLED_SOURCE_OVERLAYS` | Disable bundled plugin source bind-mount overlays |
| `OTEL_EXPORTER_OTLP_ENDPOINT` | Shared OTLP/HTTP collector endpoint for OpenTelemetry export |
| `OTEL_EXPORTER_OTLP_*_ENDPOINT` | Signal-specific OTLP endpoints for traces, metrics, or logs |
| `OTEL_EXPORTER_OTLP_PROTOCOL` | OTLP protocol override. Only `http/protobuf` is supported today |
| `OTEL_SERVICE_NAME` | Service name used for OpenTelemetry resources |
| `OTEL_SEMCONV_STABILITY_OPT_IN` | Opt in to latest experimental GenAI semantic attributes |
| `OPENCLAW_OTEL_PRELOADED` | Skip starting a second OpenTelemetry SDK when one is preloaded |
The official Docker image does not ship Homebrew. During onboarding, OpenClaw
hides brew-only skill dependency installers when it is running in a Linux
@@ -190,6 +193,15 @@ or installed manually. For dependencies available from Debian packages, use
For Python dependencies, use `OPENCLAW_IMAGE_PIP_PACKAGES`. This runs
`python3 -m pip install --break-system-packages` during the image build, so pin
package versions and use only package indexes you trust.
Source builds default `OPENCLAW_DOCKER_BUILD_NODE_OPTIONS` to
`--max-old-space-size=8192` and leave
`OPENCLAW_DOCKER_BUILD_TSDOWN_MAX_OLD_SPACE_MB` unset so the tsdown wrapper can
respect container memory limits. They also default
`OPENCLAW_DOCKER_BUILD_SKIP_DTS=1` because runtime images prune declaration
files after build. If Docker reports `ResourceExhausted`, `cannot allocate
memory`, or aborts during `tsdown`, increase the Docker builder memory limit or
retry with smaller explicit heaps, for example
`OPENCLAW_DOCKER_BUILD_NODE_OPTIONS=--max-old-space-size=4096 OPENCLAW_DOCKER_BUILD_TSDOWN_MAX_OLD_SPACE_MB=4096`.
Maintainers can test bundled plugin source against a packaged image by mounting
one plugin source directory over its packaged source path, for example
+9
View File
@@ -521,6 +521,9 @@ export OPENCLAW_IMAGE="$IMAGE_NAME"
export OPENCLAW_IMAGE_APT_PACKAGES="${OPENCLAW_IMAGE_APT_PACKAGES-${OPENCLAW_DOCKER_APT_PACKAGES:-}}"
export OPENCLAW_IMAGE_PIP_PACKAGES="${OPENCLAW_IMAGE_PIP_PACKAGES:-}"
export OPENCLAW_EXTENSIONS="${OPENCLAW_EXTENSIONS:-}"
export OPENCLAW_DOCKER_BUILD_NODE_OPTIONS="${OPENCLAW_DOCKER_BUILD_NODE_OPTIONS---max-old-space-size=8192}"
export OPENCLAW_DOCKER_BUILD_TSDOWN_MAX_OLD_SPACE_MB="${OPENCLAW_DOCKER_BUILD_TSDOWN_MAX_OLD_SPACE_MB:-}"
export OPENCLAW_DOCKER_BUILD_SKIP_DTS="${OPENCLAW_DOCKER_BUILD_SKIP_DTS:-1}"
export OPENCLAW_INSTALL_BROWSER="${OPENCLAW_INSTALL_BROWSER:-}"
export OPENCLAW_EXTRA_MOUNTS="$EXTRA_MOUNTS"
export OPENCLAW_HOME_VOLUME="$HOME_VOLUME_NAME"
@@ -726,6 +729,9 @@ upsert_env "$ENV_FILE" \
OPENCLAW_IMAGE_APT_PACKAGES \
OPENCLAW_IMAGE_PIP_PACKAGES \
OPENCLAW_EXTENSIONS \
OPENCLAW_DOCKER_BUILD_NODE_OPTIONS \
OPENCLAW_DOCKER_BUILD_TSDOWN_MAX_OLD_SPACE_MB \
OPENCLAW_DOCKER_BUILD_SKIP_DTS \
OPENCLAW_INSTALL_BROWSER \
OPENCLAW_SANDBOX \
OPENCLAW_DOCKER_SOCKET \
@@ -752,6 +758,9 @@ elif [[ "$IMAGE_NAME" == "openclaw:local" ]]; then
--build-arg "OPENCLAW_IMAGE_APT_PACKAGES=${OPENCLAW_IMAGE_APT_PACKAGES}" \
--build-arg "OPENCLAW_IMAGE_PIP_PACKAGES=${OPENCLAW_IMAGE_PIP_PACKAGES}" \
--build-arg "OPENCLAW_EXTENSIONS=${OPENCLAW_EXTENSIONS}" \
--build-arg "OPENCLAW_DOCKER_BUILD_NODE_OPTIONS=${OPENCLAW_DOCKER_BUILD_NODE_OPTIONS}" \
--build-arg "OPENCLAW_DOCKER_BUILD_TSDOWN_MAX_OLD_SPACE_MB=${OPENCLAW_DOCKER_BUILD_TSDOWN_MAX_OLD_SPACE_MB}" \
--build-arg "OPENCLAW_DOCKER_BUILD_SKIP_DTS=${OPENCLAW_DOCKER_BUILD_SKIP_DTS}" \
--build-arg "OPENCLAW_INSTALL_BROWSER=${OPENCLAW_INSTALL_BROWSER}" \
--build-arg "OPENCLAW_INSTALL_DOCKER_CLI=${OPENCLAW_INSTALL_DOCKER_CLI:-}" \
-t "$IMAGE_NAME" \
+16
View File
@@ -55,6 +55,19 @@ docker_build_transient_failure() {
"$log_file"
}
docker_build_resource_exhausted_failure() {
local log_file="$1"
grep -Eqi 'ResourceExhausted|cannot allocate memory|out of memory|exit code: 137|signal: killed|Killed' "$log_file"
}
docker_build_print_resource_exhausted_hint() {
cat >&2 <<'EOF'
Docker build failed because the builder ran out of memory.
Try increasing the Docker/BuildKit memory limit, closing other memory-heavy processes, or rebuilding with a smaller OpenClaw build heap, for example:
OPENCLAW_DOCKER_BUILD_NODE_OPTIONS=--max-old-space-size=4096 OPENCLAW_DOCKER_BUILD_TSDOWN_MAX_OLD_SPACE_MB=4096 ./scripts/docker/setup.sh
EOF
}
docker_build_retry_count() {
local configured="${OPENCLAW_DOCKER_BUILD_RETRIES:-2}"
if [[ "$configured" =~ ^[0-9]+$ ]]; then
@@ -235,6 +248,9 @@ docker_build_with_retries() {
if [ "$attempt" -ge "$max_attempts" ] || ! docker_build_transient_failure "$log_file"; then
docker_e2e_print_log "$log_file"
if docker_build_resource_exhausted_failure "$log_file"; then
docker_build_print_resource_exhausted_hint
fi
rm -f "$log_file"
return 1
fi
+8
View File
@@ -364,6 +364,9 @@ describe("scripts/docker/setup.sh", () => {
expect(result.status).toBe(0);
const envFile = await readFile(join(activeSandbox.rootDir, ".env"), "utf8");
expect(envFile).toContain("OPENCLAW_IMAGE_APT_PACKAGES=curl wget");
expect(envFile).toContain("OPENCLAW_DOCKER_BUILD_NODE_OPTIONS=--max-old-space-size=8192");
expect(envFile).toContain("OPENCLAW_DOCKER_BUILD_TSDOWN_MAX_OLD_SPACE_MB=");
expect(envFile).toContain("OPENCLAW_DOCKER_BUILD_SKIP_DTS=1");
expect(envFile).toContain("OPENCLAW_EXTRA_MOUNTS=");
expect(envFile).toContain("OPENCLAW_HOME_VOLUME=openclaw-home"); // pragma: allowlist secret
expect(envFile).toContain("OPENCLAW_DISABLE_BONJOUR=");
@@ -382,6 +385,11 @@ describe("scripts/docker/setup.sh", () => {
expect(extraCompose).toContain("openclaw-home:");
const log = await readDockerLog(activeSandbox);
expect(log).toContain("--build-arg OPENCLAW_IMAGE_APT_PACKAGES=curl wget");
expect(log).toContain(
"--build-arg OPENCLAW_DOCKER_BUILD_NODE_OPTIONS=--max-old-space-size=8192",
);
expect(log).toContain("--build-arg OPENCLAW_DOCKER_BUILD_TSDOWN_MAX_OLD_SPACE_MB=");
expect(log).toContain("--build-arg OPENCLAW_DOCKER_BUILD_SKIP_DTS=1");
expect(log).toContain(
`run --rm --no-deps ${prestartContainerEnvFlags} --entrypoint node openclaw-gateway dist/index.js onboard --mode local --no-install-daemon --gateway-auth token --gateway-token-ref-env OPENCLAW_GATEWAY_TOKEN --skip-ui --suppress-gateway-token-output`,
);
+6 -1
View File
@@ -203,7 +203,7 @@ describe("Dockerfile", () => {
"export OPENCLAW_BUILD_PRIVATE_QA=1 OPENCLAW_ENABLE_PRIVATE_QA_CLI=1",
);
const buildDockerIndex = collapsed.indexOf(
"NODE_OPTIONS=--max-old-space-size=8192 pnpm_config_verify_deps_before_run=false pnpm build:docker",
'OPENCLAW_RUN_NODE_SKIP_DTS_BUILD="$OPENCLAW_DOCKER_BUILD_SKIP_DTS" OPENCLAW_TSDOWN_MAX_OLD_SPACE_MB="$OPENCLAW_DOCKER_BUILD_TSDOWN_MAX_OLD_SPACE_MB" NODE_OPTIONS="$OPENCLAW_DOCKER_BUILD_NODE_OPTIONS" pnpm_config_verify_deps_before_run=false pnpm build:docker',
);
const qaLabBuildIndex = collapsed.indexOf(
"pnpm_config_verify_deps_before_run=false pnpm qa:lab:build",
@@ -236,6 +236,11 @@ describe("Dockerfile", () => {
const dockerfile = await readFile(dockerfilePath, "utf8");
expect(dockerfile).toContain("FROM build AS runtime-assets");
expect(dockerfile).toContain("ARG OPENCLAW_EXTENSIONS");
expect(dockerfile).toContain(
'ARG OPENCLAW_DOCKER_BUILD_NODE_OPTIONS="--max-old-space-size=8192"',
);
expect(dockerfile).toContain('ARG OPENCLAW_DOCKER_BUILD_TSDOWN_MAX_OLD_SPACE_MB=""');
expect(dockerfile).toContain("ARG OPENCLAW_DOCKER_BUILD_SKIP_DTS=1");
expect(dockerfile).toContain("ARG OPENCLAW_BUNDLED_PLUGIN_DIR");
expect(dockerfile).toContain(
"Opt-in plugin dependencies at build time (space- or comma-separated directory names).",
+27
View File
@@ -185,6 +185,7 @@ describe("docker build helper", () => {
expect(helper).toContain('docker_build_run_logged "$label" "$timeout_value" "$log_file"');
expect(helper).toContain("OPENCLAW_DOCKER_BUILD_REQUIRE_TIMEOUT");
expect(helper).toContain("frontend grpc server closed unexpectedly");
expect(helper).toContain("docker_build_resource_exhausted_failure()");
});
it("treats Docker registry auth 5xx failures as transient build failures", () => {
@@ -214,6 +215,32 @@ docker_build_transient_failure "$LOG_PATH"
}
});
it("detects Docker builder memory exhaustion failures", () => {
const workDir = mkdtempSync(join(tmpdir(), "openclaw-docker-build-memory-"));
try {
const logPath = join(workDir, "docker-build.log");
writeFileSync(
logPath,
[
'ERROR: failed to build: failed to solve: ResourceExhausted: process "/bin/sh -c pnpm build:docker" did not complete successfully: cannot allocate memory',
].join("\n"),
);
const rootDir = process.cwd();
const script = `
set -euo pipefail
ROOT_DIR=${shellQuote(rootDir)}
LOG_PATH=${shellQuote(logPath)}
source "$ROOT_DIR/scripts/lib/docker-build.sh"
docker_build_resource_exhausted_failure "$LOG_PATH"
`;
execFileSync("bash", ["-lc", script], { encoding: "utf8" });
} finally {
rmSync(workDir, { recursive: true, force: true });
}
});
it("keeps shell-script Docker builds behind the helper", () => {
for (const path of CENTRALIZED_BUILD_SCRIPTS) {
const script = readFileSync(path, "utf8");
+7 -9
View File
@@ -1,13 +1,6 @@
// Test Install Sh Docker tests cover test install sh docker script behavior.
import { spawn, spawnSync } from "node:child_process";
import {
existsSync,
mkdirSync,
mkdtempSync,
readFileSync,
rmSync,
writeFileSync,
} from "node:fs";
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import path, { join } from "node:path";
import { runInNewContext } from "node:vm";
@@ -448,7 +441,12 @@ describe("test-install-sh-docker", () => {
const dockerfile = readFileSync("Dockerfile", "utf8");
expect(dockerfile).toContain(
"NODE_OPTIONS=--max-old-space-size=8192 pnpm_config_verify_deps_before_run=false pnpm build:docker",
'ARG OPENCLAW_DOCKER_BUILD_NODE_OPTIONS="--max-old-space-size=8192"',
);
expect(dockerfile).toContain('ARG OPENCLAW_DOCKER_BUILD_TSDOWN_MAX_OLD_SPACE_MB=""');
expect(dockerfile).toContain("ARG OPENCLAW_DOCKER_BUILD_SKIP_DTS=1");
expect(dockerfile).toContain(
'OPENCLAW_RUN_NODE_SKIP_DTS_BUILD="$OPENCLAW_DOCKER_BUILD_SKIP_DTS" OPENCLAW_TSDOWN_MAX_OLD_SPACE_MB="$OPENCLAW_DOCKER_BUILD_TSDOWN_MAX_OLD_SPACE_MB" NODE_OPTIONS="$OPENCLAW_DOCKER_BUILD_NODE_OPTIONS" pnpm_config_verify_deps_before_run=false pnpm build:docker',
);
});