From 57f19f0d5c10a54319e3d05f000c1213f17f3601 Mon Sep 17 00:00:00 2001 From: Sally O'Malley Date: Fri, 6 Mar 2026 12:18:42 -0500 Subject: [PATCH] container builds: opt-in extension deps via OPENCLAW_EXTENSIONS build arg (#32223) * Docker: opt-in extension deps via OPENCLAW_EXTENSIONS build arg Co-Authored-By: Claude Opus 4.6 Signed-off-by: sallyom * CI: clarify extension smoke scope * Tests: allow digest-pinned multi-stage FROM lines * Changelog: note container extension preinstall option --------- Signed-off-by: sallyom Co-authored-by: Claude Opus 4.6 Co-authored-by: Vincent Koc --- .github/workflows/install-smoke.yml | 10 ++++++++++ CHANGELOG.md | 1 + Dockerfile | 21 +++++++++++++++++++++ docker-setup.sh | 3 +++ docs/install/docker.md | 26 ++++++++++++++++++++++++++ docs/install/podman.md | 5 +++++ setup-podman.sh | 5 ++++- src/docker-image-digests.test.ts | 2 +- 8 files changed, 71 insertions(+), 2 deletions(-) diff --git a/.github/workflows/install-smoke.yml b/.github/workflows/install-smoke.yml index 2f16f294d..682c240a1 100644 --- a/.github/workflows/install-smoke.yml +++ b/.github/workflows/install-smoke.yml @@ -41,6 +41,16 @@ jobs: docker build -t openclaw-dockerfile-smoke:local -f Dockerfile . docker run --rm --entrypoint sh openclaw-dockerfile-smoke:local -lc 'which openclaw && openclaw --version' + # This smoke only validates that the build-arg path preinstalls selected + # extension deps without breaking image build or basic CLI startup. It + # does not exercise runtime loading/registration of diagnostics-otel. + - name: Smoke test Dockerfile with extension build arg + run: | + docker build \ + --build-arg OPENCLAW_EXTENSIONS="diagnostics-otel" \ + -t openclaw-ext-smoke:local -f Dockerfile . + docker run --rm --entrypoint sh openclaw-ext-smoke:local -lc 'which openclaw && openclaw --version' + - name: Run installer docker tests env: CLAWDBOT_INSTALL_URL: https://openclaw.ai/install.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index 376a792e5..cf05272cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ Docs: https://docs.openclaw.ai - Hooks/Compaction lifecycle: emit `session:compact:before` and `session:compact:after` internal events plus plugin compaction callbacks with session/count metadata, so automations can react to compaction runs consistently. (#16788) thanks @vincentkoc. - Agents/context engine plugin interface: add `ContextEngine` plugin slot with full lifecycle hooks (`bootstrap`, `ingest`, `assemble`, `compact`, `afterTurn`, `prepareSubagentSpawn`, `onSubagentEnded`), slot-based registry with config-driven resolution, `LegacyContextEngine` wrapper preserving existing compaction behavior, scoped subagent runtime for plugin runtimes via `AsyncLocalStorage`, and `sessions.get` gateway method. Enables plugins like `lossless-claw` to provide alternative context management strategies without modifying core compaction logic. Zero behavior change when no context engine plugin is configured. (#22201) thanks @jalehman. - CLI: make read-only SecretRef status flows degrade safely (#37023) thanks @joshavant. +- Docker/Podman extension dependency baking: add `OPENCLAW_EXTENSIONS` so container builds can preinstall selected bundled extension npm dependencies into the image for faster and more reproducible startup in container deployments. (#32223) Thanks @sallyom. ### Breaking diff --git a/Dockerfile b/Dockerfile index b314ca328..3b51860cf 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,3 +1,22 @@ +# Opt-in extension dependencies at build time (space-separated directory names). +# Example: docker build --build-arg OPENCLAW_EXTENSIONS="diagnostics-otel matrix" . +# +# A multi-stage build is used instead of `RUN --mount=type=bind` because +# bind mounts require BuildKit, which is not available in plain Docker. +# This stage extracts only the package.json files we need from extensions/, +# so the main build layer is not invalidated by unrelated extension source changes. +ARG OPENCLAW_EXTENSIONS="" +FROM node:22-bookworm@sha256:cd7bcd2e7a1e6f72052feb023c7f6b722205d3fcab7bbcbd2d1bfdab10b1e935 AS ext-deps +ARG OPENCLAW_EXTENSIONS +COPY extensions /tmp/extensions +RUN mkdir -p /out && \ + for ext in $OPENCLAW_EXTENSIONS; do \ + if [ -f "/tmp/extensions/$ext/package.json" ]; then \ + mkdir -p "/out/$ext" && \ + cp "/tmp/extensions/$ext/package.json" "/out/$ext/package.json"; \ + fi; \ + done + FROM node:22-bookworm@sha256:cd7bcd2e7a1e6f72052feb023c7f6b722205d3fcab7bbcbd2d1bfdab10b1e935 # OCI base-image metadata for downstream image consumers. @@ -35,6 +54,8 @@ COPY --chown=node:node ui/package.json ./ui/package.json COPY --chown=node:node patches ./patches COPY --chown=node:node scripts ./scripts +COPY --from=ext-deps --chown=node:node /out/ ./extensions/ + USER node # Reduce OOM risk on low-memory hosts during dependency installation. # Docker builds on small VMs may otherwise fail with "Killed" (exit 137). diff --git a/docker-setup.sh b/docker-setup.sh index ce5e6a08f..205394ff3 100755 --- a/docker-setup.sh +++ b/docker-setup.sh @@ -200,6 +200,7 @@ export OPENCLAW_BRIDGE_PORT="${OPENCLAW_BRIDGE_PORT:-18790}" export OPENCLAW_GATEWAY_BIND="${OPENCLAW_GATEWAY_BIND:-lan}" export OPENCLAW_IMAGE="$IMAGE_NAME" export OPENCLAW_DOCKER_APT_PACKAGES="${OPENCLAW_DOCKER_APT_PACKAGES:-}" +export OPENCLAW_EXTENSIONS="${OPENCLAW_EXTENSIONS:-}" export OPENCLAW_EXTRA_MOUNTS="$EXTRA_MOUNTS" export OPENCLAW_HOME_VOLUME="$HOME_VOLUME_NAME" export OPENCLAW_ALLOW_INSECURE_PRIVATE_WS="${OPENCLAW_ALLOW_INSECURE_PRIVATE_WS:-}" @@ -378,6 +379,7 @@ upsert_env "$ENV_FILE" \ OPENCLAW_EXTRA_MOUNTS \ OPENCLAW_HOME_VOLUME \ OPENCLAW_DOCKER_APT_PACKAGES \ + OPENCLAW_EXTENSIONS \ OPENCLAW_SANDBOX \ OPENCLAW_DOCKER_SOCKET \ DOCKER_GID \ @@ -388,6 +390,7 @@ if [[ "$IMAGE_NAME" == "openclaw:local" ]]; then echo "==> Building Docker image: $IMAGE_NAME" docker build \ --build-arg "OPENCLAW_DOCKER_APT_PACKAGES=${OPENCLAW_DOCKER_APT_PACKAGES}" \ + --build-arg "OPENCLAW_EXTENSIONS=${OPENCLAW_EXTENSIONS}" \ --build-arg "OPENCLAW_INSTALL_DOCKER_CLI=${OPENCLAW_INSTALL_DOCKER_CLI:-}" \ -t "$IMAGE_NAME" \ -f "$ROOT_DIR/Dockerfile" \ diff --git a/docs/install/docker.md b/docs/install/docker.md index 0b6181376..8cbf2555e 100644 --- a/docs/install/docker.md +++ b/docs/install/docker.md @@ -60,6 +60,7 @@ Optional env vars: - `OPENCLAW_IMAGE` — use a remote image instead of building locally (e.g. `ghcr.io/openclaw/openclaw:latest`) - `OPENCLAW_DOCKER_APT_PACKAGES` — install extra apt packages during build +- `OPENCLAW_EXTENSIONS` — pre-install extension dependencies at build time (space-separated extension names, e.g. `diagnostics-otel matrix`) - `OPENCLAW_EXTRA_MOUNTS` — add extra host bind mounts - `OPENCLAW_HOME_VOLUME` — persist `/home/node` in a named volume - `OPENCLAW_SANDBOX` — opt in to Docker gateway sandbox bootstrap. Only explicit truthy values enable it: `1`, `true`, `yes`, `on` @@ -320,6 +321,31 @@ Notes: - If you change `OPENCLAW_DOCKER_APT_PACKAGES`, rerun `docker-setup.sh` to rebuild the image. +### Pre-install extension dependencies (optional) + +Extensions with their own `package.json` (e.g. `diagnostics-otel`, `matrix`, +`msteams`) install their npm dependencies on first load. To bake those +dependencies into the image instead, set `OPENCLAW_EXTENSIONS` before +running `docker-setup.sh`: + +```bash +export OPENCLAW_EXTENSIONS="diagnostics-otel matrix" +./docker-setup.sh +``` + +Or when building directly: + +```bash +docker build --build-arg OPENCLAW_EXTENSIONS="diagnostics-otel matrix" . +``` + +Notes: + +- This accepts a space-separated list of extension directory names (under `extensions/`). +- Only extensions with a `package.json` are affected; lightweight plugins without one are ignored. +- If you change `OPENCLAW_EXTENSIONS`, rerun `docker-setup.sh` to rebuild + the image. + ### Power-user / full-featured container (opt-in) The default Docker image is **security-first** and runs as the non-root `node` diff --git a/docs/install/podman.md b/docs/install/podman.md index 707fdd3a1..e753c82f3 100644 --- a/docs/install/podman.md +++ b/docs/install/podman.md @@ -32,6 +32,11 @@ By default the container is **not** installed as a systemd service, you start it (Or set `OPENCLAW_PODMAN_QUADLET=1`; use `--container` to install only the container and launch script.) +Optional build-time env vars (set before running `setup-podman.sh`): + +- `OPENCLAW_DOCKER_APT_PACKAGES` — install extra apt packages during image build +- `OPENCLAW_EXTENSIONS` — pre-install extension dependencies (space-separated extension names, e.g. `diagnostics-otel matrix`) + **2. Start gateway** (manual, for quick smoke testing): ```bash diff --git a/setup-podman.sh b/setup-podman.sh index 0079b3eeb..8b9c5caab 100755 --- a/setup-podman.sh +++ b/setup-podman.sh @@ -209,7 +209,10 @@ if ! run_as_openclaw test -f "$OPENCLAW_JSON"; then fi echo "Building image from $REPO_PATH..." -podman build -t openclaw:local -f "$REPO_PATH/Dockerfile" "$REPO_PATH" +BUILD_ARGS=() +[[ -n "${OPENCLAW_DOCKER_APT_PACKAGES:-}" ]] && BUILD_ARGS+=(--build-arg "OPENCLAW_DOCKER_APT_PACKAGES=${OPENCLAW_DOCKER_APT_PACKAGES}") +[[ -n "${OPENCLAW_EXTENSIONS:-}" ]] && BUILD_ARGS+=(--build-arg "OPENCLAW_EXTENSIONS=${OPENCLAW_EXTENSIONS}") +podman build ${BUILD_ARGS[@]+"${BUILD_ARGS[@]}"} -t openclaw:local -f "$REPO_PATH/Dockerfile" "$REPO_PATH" echo "Loading image into $OPENCLAW_USER's Podman store..." TMP_IMAGE="$(mktemp -p /tmp openclaw-image.XXXXXX.tar)" diff --git a/src/docker-image-digests.test.ts b/src/docker-image-digests.test.ts index ab721e5ab..d62a46434 100644 --- a/src/docker-image-digests.test.ts +++ b/src/docker-image-digests.test.ts @@ -42,7 +42,7 @@ describe("docker base image pinning", () => { .find((line) => line.trimStart().startsWith("FROM ")); expect(fromLine, `${dockerfilePath} should define a FROM line`).toBeDefined(); expect(fromLine, `${dockerfilePath} FROM must be digest-pinned`).toMatch( - /^FROM\s+\S+@sha256:[a-f0-9]{64}$/, + /^FROM\s+\S+@sha256:[a-f0-9]{64}(?:\s+AS\s+\S+)?$/, ); } });