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 <noreply@anthropic.com>
Signed-off-by: sallyom <somalley@redhat.com>

* CI: clarify extension smoke scope

* Tests: allow digest-pinned multi-stage FROM lines

* Changelog: note container extension preinstall option

---------

Signed-off-by: sallyom <somalley@redhat.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
This commit is contained in:
Sally O'Malley
2026-03-06 12:18:42 -05:00
committed by GitHub
parent d070c44091
commit 57f19f0d5c
8 changed files with 71 additions and 2 deletions

View File

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

View File

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

View File

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

View File

@@ -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" \

View File

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

View File

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

View File

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

View File

@@ -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+)?$/,
);
}
});