From 47b8e56e3f7bcacfb776f0705fbdd7bb88ec17c6 Mon Sep 17 00:00:00 2001 From: Said Urtabajev Date: Mon, 18 May 2026 08:37:16 +0300 Subject: [PATCH] feat(docker): add image apt package build arg feat(docker): add image apt package build arg Add OPENCLAW_IMAGE_APT_PACKAGES as the preferred runtime-neutral image build arg for Docker and Podman apt package installs while keeping OPENCLAW_DOCKER_APT_PACKAGES as the legacy fallback. Maintainer verification: - pnpm docs:list - node scripts/run-vitest.mjs run --config test/vitest/vitest.e2e.config.ts src/docker-setup.e2e.test.ts - node scripts/run-vitest.mjs src/dockerfile.test.ts test/scripts/test-install-sh-docker.test.ts - node scripts/run-vitest.mjs run --config test/vitest/vitest.cron.config.ts src/cron/isolated-agent.model-overrides.test.ts - pnpm exec oxfmt --check --threads=1 docs/install/docker.md docs/install/podman.md scripts/clawdock/README.md docs/help/faq.md CHANGELOG.md - git diff --check origin/main...HEAD - .agents/skills/autoreview/scripts/autoreview --mode local - .agents/skills/autoreview/scripts/autoreview --mode branch - pnpm check:changed via Blacksmith Testbox tbx_01krwqmfhcdekaczvrkxnb7t59, Actions run 26014630478, exit 0 Known CI note: checks-node-core-runtime-shared timed out repeatedly in unrelated src/cron/isolated-agent.model-overrides.test.ts on GitHub Actions; the same test passes locally after this rebase. Co-authored-by: Said Urtabajev --- CHANGELOG.md | 1 + Dockerfile | 9 +++-- docs/help/faq.md | 2 +- docs/install/docker.md | 41 +++++++++---------- docs/install/podman.md | 2 +- scripts/clawdock/README.md | 2 +- scripts/docker/setup.sh | 6 +-- scripts/podman/setup.sh | 5 ++- src/docker-setup.e2e.test.ts | 76 ++++++++++++++++++++++++++++++++++-- 9 files changed, 110 insertions(+), 34 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 43fb80fbe81..899ddde5012 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ Docs: https://docs.openclaw.ai ### Changes - Dependencies: update Pi packages to 0.75.1 and raise the minimum supported Node.js 22 line to 22.19. +- Docker/Podman: add `OPENCLAW_IMAGE_APT_PACKAGES` as the runtime-neutral image build arg for extra apt packages while keeping `OPENCLAW_DOCKER_APT_PACKAGES` as a legacy fallback. (#62431) Thanks @urtabajev. - Mac app: redesign Settings pages with consistent card layouts, cached navigation, cleaner permissions/voice/skills/cron/exec/debug panes, and steadier spacing around the native sidebar. - Skills: rename the repo-local Codex closeout review skill and helper to `autoreview` while preserving the Codex-first fallback behavior. - Skills: add a meme-maker skill for curated template search, local SVG/PNG rendering, Imgflip hosted rendering, and Know Your Meme provenance links. diff --git a/Dockerfile b/Dockerfile index 9ba3b1a978f..8695ff68e06 100644 --- a/Dockerfile +++ b/Dockerfile @@ -198,13 +198,16 @@ RUN install -d -m 0755 "$COREPACK_HOME" && \ chmod -R a+rX "$COREPACK_HOME" # Install additional system packages needed by your skills or extensions. -# Example: docker build --build-arg OPENCLAW_DOCKER_APT_PACKAGES="python3 wget" . +# Example: docker build --build-arg OPENCLAW_IMAGE_APT_PACKAGES="python3 wget" . +# Legacy alias: OPENCLAW_DOCKER_APT_PACKAGES is still accepted as a fallback. +ARG OPENCLAW_IMAGE_APT_PACKAGES ARG OPENCLAW_DOCKER_APT_PACKAGES="" RUN --mount=type=cache,id=openclaw-bookworm-apt-cache,target=/var/cache/apt,sharing=locked \ --mount=type=cache,id=openclaw-bookworm-apt-lists,target=/var/lib/apt,sharing=locked \ - if [ -n "$OPENCLAW_DOCKER_APT_PACKAGES" ]; then \ + packages="${OPENCLAW_IMAGE_APT_PACKAGES-$OPENCLAW_DOCKER_APT_PACKAGES}"; \ + if [ -n "$packages" ]; then \ apt-get update && \ - DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends $OPENCLAW_DOCKER_APT_PACKAGES; \ + DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends $packages; \ fi # Optionally install Chromium and Xvfb for browser automation. diff --git a/docs/help/faq.md b/docs/help/faq.md index f40a8621132..7da72b9a59a 100644 --- a/docs/help/faq.md +++ b/docs/help/faq.md @@ -451,7 +451,7 @@ lives on the [First-run FAQ](/help/faq-first-run). include system packages, Homebrew, or bundled browsers. For a fuller setup: - Persist `/home/node` with `OPENCLAW_HOME_VOLUME` so caches survive. - - Bake system deps into the image with `OPENCLAW_DOCKER_APT_PACKAGES`. + - Bake system deps into the image with `OPENCLAW_IMAGE_APT_PACKAGES`. - Install Playwright browsers via the bundled CLI: `node /app/node_modules/playwright-core/cli.js install chromium` - Set `PLAYWRIGHT_BROWSERS_PATH` and ensure the path is persisted. diff --git a/docs/install/docker.md b/docs/install/docker.md index 36c0734e13e..806f095105f 100644 --- a/docs/install/docker.md +++ b/docs/install/docker.md @@ -123,30 +123,31 @@ 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_DOCKER_APT_PACKAGES` | Install extra apt packages during build (space-separated) | -| `OPENCLAW_EXTENSIONS` | Include selected bundled plugin helpers at build time | -| `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_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 | The official Docker image does not ship Homebrew. During onboarding, OpenClaw hides brew-only skill dependency installers when it is running in a Linux container without `brew`; those dependencies must be provided by a custom image or installed manually. For dependencies available from Debian packages, use -`OPENCLAW_DOCKER_APT_PACKAGES` during image build. +`OPENCLAW_IMAGE_APT_PACKAGES` during image build. The legacy +`OPENCLAW_DOCKER_APT_PACKAGES` name is still accepted. Maintainers can test bundled plugin source against a packaged image by mounting one plugin source directory over its packaged source path, for example @@ -421,7 +422,7 @@ See [ClawDock](/install/clawdock) for the full helper guide. full-featured container: 1. **Persist `/home/node`**: `export OPENCLAW_HOME_VOLUME="openclaw_home"` - 2. **Bake system deps**: `export OPENCLAW_DOCKER_APT_PACKAGES="git curl jq"` + 2. **Bake system deps**: `export OPENCLAW_IMAGE_APT_PACKAGES="git curl jq"` 3. **Bake Playwright Chromium**: `export OPENCLAW_INSTALL_BROWSER=1` 4. **Or install Playwright browsers into a persisted volume**: ```bash diff --git a/docs/install/podman.md b/docs/install/podman.md index 15ad89fa0e7..5c663a85f56 100644 --- a/docs/install/podman.md +++ b/docs/install/podman.md @@ -61,7 +61,7 @@ You can also set `OPENCLAW_PODMAN_QUADLET=1`. Optional build/setup env vars: - `OPENCLAW_IMAGE` or `OPENCLAW_PODMAN_IMAGE` -- use an existing/pulled image instead of building `openclaw:local` -- `OPENCLAW_DOCKER_APT_PACKAGES` -- install extra apt packages during image build +- `OPENCLAW_IMAGE_APT_PACKAGES` -- install extra apt packages during image build (also accepts legacy `OPENCLAW_DOCKER_APT_PACKAGES`) - `OPENCLAW_EXTENSIONS` -- pre-install plugin dependencies at build time - `OPENCLAW_INSTALL_BROWSER` -- pre-install Chromium and Xvfb for browser automation (set to `1` to enable) diff --git a/scripts/clawdock/README.md b/scripts/clawdock/README.md index db77621e784..d27d05ddcec 100644 --- a/scripts/clawdock/README.md +++ b/scripts/clawdock/README.md @@ -182,7 +182,7 @@ See `.env.example` for all supported keys. The `Dockerfile` supports two optional build args: -- `OPENCLAW_DOCKER_APT_PACKAGES` — extra apt packages to install (e.g. `ffmpeg`) +- `OPENCLAW_IMAGE_APT_PACKAGES` — extra apt packages to install (e.g. `ffmpeg`); also accepts legacy `OPENCLAW_DOCKER_APT_PACKAGES` - `OPENCLAW_INSTALL_BROWSER=1` — pre-install Chromium for browser automation (adds ~300MB, but skips the 60-90s Playwright install on each container start) ### How It Works in Docker diff --git a/scripts/docker/setup.sh b/scripts/docker/setup.sh index 6d95ea4ac1f..0318acf6744 100755 --- a/scripts/docker/setup.sh +++ b/scripts/docker/setup.sh @@ -296,7 +296,7 @@ export OPENCLAW_BRIDGE_PORT="${OPENCLAW_BRIDGE_PORT:-18790}" export OPENCLAW_GATEWAY_BIND="${OPENCLAW_GATEWAY_BIND:-lan}" export OPENCLAW_DISABLE_BONJOUR="${OPENCLAW_DISABLE_BONJOUR:-}" export OPENCLAW_IMAGE="$IMAGE_NAME" -export OPENCLAW_DOCKER_APT_PACKAGES="${OPENCLAW_DOCKER_APT_PACKAGES:-}" +export OPENCLAW_IMAGE_APT_PACKAGES="${OPENCLAW_IMAGE_APT_PACKAGES-${OPENCLAW_DOCKER_APT_PACKAGES:-}}" export OPENCLAW_EXTENSIONS="${OPENCLAW_EXTENSIONS:-}" export OPENCLAW_INSTALL_BROWSER="${OPENCLAW_INSTALL_BROWSER:-}" export OPENCLAW_EXTRA_MOUNTS="$EXTRA_MOUNTS" @@ -500,7 +500,7 @@ upsert_env "$ENV_FILE" \ OPENCLAW_IMAGE \ OPENCLAW_EXTRA_MOUNTS \ OPENCLAW_HOME_VOLUME \ - OPENCLAW_DOCKER_APT_PACKAGES \ + OPENCLAW_IMAGE_APT_PACKAGES \ OPENCLAW_EXTENSIONS \ OPENCLAW_INSTALL_BROWSER \ OPENCLAW_SANDBOX \ @@ -522,7 +522,7 @@ upsert_env "$ENV_FILE" \ if [[ "$IMAGE_NAME" == "openclaw:local" ]]; then echo "==> Building Docker image: $IMAGE_NAME" run_docker_build \ - --build-arg "OPENCLAW_DOCKER_APT_PACKAGES=${OPENCLAW_DOCKER_APT_PACKAGES}" \ + --build-arg "OPENCLAW_IMAGE_APT_PACKAGES=${OPENCLAW_IMAGE_APT_PACKAGES}" \ --build-arg "OPENCLAW_EXTENSIONS=${OPENCLAW_EXTENSIONS}" \ --build-arg "OPENCLAW_INSTALL_BROWSER=${OPENCLAW_INSTALL_BROWSER}" \ --build-arg "OPENCLAW_INSTALL_DOCKER_CLI=${OPENCLAW_INSTALL_DOCKER_CLI:-}" \ diff --git a/scripts/podman/setup.sh b/scripts/podman/setup.sh index 4c59831901c..8cc3d5f0039 100755 --- a/scripts/podman/setup.sh +++ b/scripts/podman/setup.sh @@ -358,9 +358,10 @@ install -d -m 700 "$OPENCLAW_CONFIG_DIR" "$OPENCLAW_WORKSPACE_DIR" ensure_private_existing_dir_owned_by_user "config directory" "$OPENCLAW_CONFIG_DIR" ensure_private_existing_dir_owned_by_user "workspace directory" "$OPENCLAW_WORKSPACE_DIR" +OPENCLAW_IMAGE_APT_PACKAGES="${OPENCLAW_IMAGE_APT_PACKAGES-${OPENCLAW_DOCKER_APT_PACKAGES:-}}" BUILD_ARGS=() -if [[ -n "${OPENCLAW_DOCKER_APT_PACKAGES:-}" ]]; then - BUILD_ARGS+=(--build-arg "OPENCLAW_DOCKER_APT_PACKAGES=${OPENCLAW_DOCKER_APT_PACKAGES}") +if [[ -n "$OPENCLAW_IMAGE_APT_PACKAGES" ]]; then + BUILD_ARGS+=(--build-arg "OPENCLAW_IMAGE_APT_PACKAGES=${OPENCLAW_IMAGE_APT_PACKAGES}") fi if [[ -n "${OPENCLAW_EXTENSIONS:-}" ]]; then BUILD_ARGS+=(--build-arg "OPENCLAW_EXTENSIONS=${OPENCLAW_EXTENSIONS}") diff --git a/src/docker-setup.e2e.test.ts b/src/docker-setup.e2e.test.ts index bd4b709857d..ba8b79ef0d2 100644 --- a/src/docker-setup.e2e.test.ts +++ b/src/docker-setup.e2e.test.ts @@ -258,13 +258,13 @@ describe("scripts/docker/setup.sh", () => { const activeSandbox = requireSandbox(sandbox); const result = runDockerSetup(activeSandbox, { - OPENCLAW_DOCKER_APT_PACKAGES: "ffmpeg build-essential", + OPENCLAW_DOCKER_APT_PACKAGES: "curl wget", OPENCLAW_EXTRA_MOUNTS: undefined, OPENCLAW_HOME_VOLUME: "openclaw-home", }); expect(result.status).toBe(0); const envFile = await readFile(join(activeSandbox.rootDir, ".env"), "utf8"); - expect(envFile).toContain("OPENCLAW_DOCKER_APT_PACKAGES=ffmpeg build-essential"); + expect(envFile).toContain("OPENCLAW_IMAGE_APT_PACKAGES=curl wget"); expect(envFile).toContain("OPENCLAW_EXTRA_MOUNTS="); expect(envFile).toContain("OPENCLAW_HOME_VOLUME=openclaw-home"); // pragma: allowlist secret expect(envFile).toContain("OPENCLAW_DISABLE_BONJOUR="); @@ -282,7 +282,7 @@ describe("scripts/docker/setup.sh", () => { expect(extraCompose).toContain("volumes:"); expect(extraCompose).toContain("openclaw-home:"); const log = await readDockerLog(activeSandbox); - expect(log).toContain("--build-arg OPENCLAW_DOCKER_APT_PACKAGES=ffmpeg build-essential"); + expect(log).toContain("--build-arg OPENCLAW_IMAGE_APT_PACKAGES=curl wget"); expect(log).toContain( `run --rm --no-deps ${prestartContainerEnvFlags} --entrypoint node openclaw-gateway dist/index.js onboard --mode local --no-install-daemon`, ); @@ -304,6 +304,61 @@ describe("scripts/docker/setup.sh", () => { expect(envFile).toContain("OPENCLAW_DISABLE_BONJOUR=0"); }); + it("normalizes legacy OPENCLAW_DOCKER_APT_PACKAGES into OPENCLAW_IMAGE_APT_PACKAGES", async () => { + const activeSandbox = requireSandbox(sandbox); + await resetDockerLog(activeSandbox); + + const result = runDockerSetup(activeSandbox, { + OPENCLAW_DOCKER_APT_PACKAGES: "curl wget", + }); + 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).not.toContain("OPENCLAW_DOCKER_APT_PACKAGES"); + + const log = await readDockerLog(activeSandbox); + expect(log).toContain("--build-arg OPENCLAW_IMAGE_APT_PACKAGES=curl wget"); + expect(log).not.toContain("--build-arg OPENCLAW_DOCKER_APT_PACKAGES"); + }); + + it("prefers OPENCLAW_IMAGE_APT_PACKAGES over legacy OPENCLAW_DOCKER_APT_PACKAGES", async () => { + const activeSandbox = requireSandbox(sandbox); + await resetDockerLog(activeSandbox); + + const result = runDockerSetup(activeSandbox, { + OPENCLAW_IMAGE_APT_PACKAGES: "curl wget httpie", + OPENCLAW_DOCKER_APT_PACKAGES: "curl wget", + }); + expect(result.status).toBe(0); + + const envFile = await readFile(join(activeSandbox.rootDir, ".env"), "utf8"); + expect(envFile).toContain("OPENCLAW_IMAGE_APT_PACKAGES=curl wget httpie"); + expect(envFile).not.toContain("OPENCLAW_DOCKER_APT_PACKAGES"); + + const log = await readDockerLog(activeSandbox); + expect(log).toContain("--build-arg OPENCLAW_IMAGE_APT_PACKAGES=curl wget httpie"); + expect(log).not.toMatch(/--build-arg OPENCLAW_IMAGE_APT_PACKAGES=curl wget(?! httpie)/); + }); + + it("explicitly empty OPENCLAW_IMAGE_APT_PACKAGES suppresses legacy fallback", async () => { + const activeSandbox = requireSandbox(sandbox); + await resetDockerLog(activeSandbox); + + const result = runDockerSetup(activeSandbox, { + OPENCLAW_IMAGE_APT_PACKAGES: "", + OPENCLAW_DOCKER_APT_PACKAGES: "curl wget", + }); + expect(result.status).toBe(0); + + const envFile = await readFile(join(activeSandbox.rootDir, ".env"), "utf8"); + expect(envFile).toContain("OPENCLAW_IMAGE_APT_PACKAGES="); + expect(envFile).not.toContain("curl wget"); + + const log = await readDockerLog(activeSandbox); + expect(log).not.toContain("--build-arg OPENCLAW_IMAGE_APT_PACKAGES=curl wget"); + }); + it("avoids shared-network openclaw-cli before the gateway is started", async () => { const activeSandbox = requireSandbox(sandbox); @@ -742,4 +797,19 @@ describe("scripts/docker/setup.sh", () => { compose.match(/OPENCLAW_WORKSPACE_DIR: \/home\/node\/\.openclaw\/workspace$/gm), ).toHaveLength(2); }); + + it("Dockerfile ARG OPENCLAW_IMAGE_APT_PACKAGES must not have a default value", async () => { + // If the ARG has a default (e.g. ARG OPENCLAW_IMAGE_APT_PACKAGES=""), Docker treats it as + // "set" even when no --build-arg is passed. That breaks the RUN fallback expression + // ${OPENCLAW_IMAGE_APT_PACKAGES-$OPENCLAW_DOCKER_APT_PACKAGES} because the variable is + // never truly unset, so legacy-only callers using --build-arg OPENCLAW_DOCKER_APT_PACKAGES + // get nothing installed — a backward-compat regression. + const dockerfile = await readFile(join(repoRoot, "Dockerfile"), "utf8"); + const argLine = dockerfile + .split("\n") + .find((line) => line.startsWith("ARG OPENCLAW_IMAGE_APT_PACKAGES")); + expect(argLine).toBeDefined(); + // Must be bare `ARG OPENCLAW_IMAGE_APT_PACKAGES` with no default assignment + expect(argLine).toBe("ARG OPENCLAW_IMAGE_APT_PACKAGES"); + }); });