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 <said@bumpclub.ee>
This commit is contained in:
Said Urtabajev
2026-05-18 08:37:16 +03:00
committed by GitHub
parent 57bc26893e
commit 47b8e56e3f
9 changed files with 110 additions and 34 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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