mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-21 10:16:44 +00:00
fix(docker): reduce local build memory pressure
This commit is contained in:
+7
-1
@@ -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
@@ -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
|
||||
|
||||
@@ -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" \
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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`,
|
||||
);
|
||||
|
||||
@@ -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).",
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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',
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user