From 7b721ca85ed4a7c31b8903e2767cbef0f40ffaed Mon Sep 17 00:00:00 2001 From: George Pickett Date: Wed, 25 Feb 2026 10:54:54 -0800 Subject: [PATCH] fix: docker setup supports non-local prebuilt images safely (#7986) (thanks @ozbillwang) --- CHANGELOG.md | 1 + docker-setup.sh | 6 +++-- docs/install/docker.md | 18 ++++++++++++- src/docker-setup.test.ts | 56 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 78 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6efa7d35cb39..bc1aaf2b379b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- Docker/Setup: when `OPENCLAW_IMAGE` is set to a non-local image, `docker-setup.sh` now pulls it instead of building locally, and fails fast if `OPENCLAW_DOCKER_APT_PACKAGES` is set in pull mode. (#7986) Thanks @ozbillwang. - Security/Nextcloud Talk: reject unsigned webhook traffic before full body reads, reducing unauthenticated request-body exposure, with auth-order regression coverage. (#26118) Thanks @bmendonca3. - Security/Nextcloud Talk: stop treating DM pairing-store entries as group allowlist senders, so group authorization remains bounded to configured group allowlists. (#26116) Thanks @bmendonca3. - Security/IRC: keep pairing-store approvals DM-only and out of IRC group allowlist authorization, with policy regression tests for allowlist resolution. (#26112) Thanks @bmendonca3. diff --git a/docker-setup.sh b/docker-setup.sh index c0cd925c4c3e..531d094fc3f1 100755 --- a/docker-setup.sh +++ b/docker-setup.sh @@ -255,10 +255,12 @@ if [[ "$IMAGE_NAME" == "openclaw:local" ]]; then -f "$ROOT_DIR/Dockerfile" \ "$ROOT_DIR" else + if [[ -n "$OPENCLAW_DOCKER_APT_PACKAGES" ]]; then + fail "OPENCLAW_DOCKER_APT_PACKAGES is build-only and cannot be used when OPENCLAW_IMAGE is not openclaw:local." + fi echo "==> Pulling Docker image: $IMAGE_NAME" if ! docker pull "$IMAGE_NAME"; then - echo "ERROR: Failed to pull image $IMAGE_NAME. Please check the image name and your access permissions." >&2 - exit 1 + fail "Failed to pull image $IMAGE_NAME. Check the image name and access permissions." fi fi diff --git a/docs/install/docker.md b/docs/install/docker.md index decd1d779ee7..60ded497488f 100644 --- a/docs/install/docker.md +++ b/docs/install/docker.md @@ -40,7 +40,7 @@ From repo root: This script: -- builds the gateway image +- builds the gateway image (default), or pulls a prebuilt image when `OPENCLAW_IMAGE` is set to a non-local image - runs the onboarding wizard - prints optional provider setup hints - starts the gateway via Docker Compose @@ -91,6 +91,21 @@ docker compose run --rm openclaw-cli onboard docker compose up -d openclaw-gateway ``` +### Use a prebuilt image (optional) + +To skip local builds and use a prebuilt image, set `OPENCLAW_IMAGE` to any image +other than `openclaw:local` before running `docker-setup.sh`: + +```bash +export OPENCLAW_IMAGE="ghcr.io/openclaw/openclaw:main" +./docker-setup.sh +``` + +Notes: + +- `OPENCLAW_DOCKER_APT_PACKAGES` is build-only and cannot be combined with + non-local `OPENCLAW_IMAGE` values. + Note: run `docker compose ...` from the repo root. If you enabled `OPENCLAW_EXTRA_MOUNTS` or `OPENCLAW_HOME_VOLUME`, the setup script writes `docker-compose.extra.yml`; include it when running Compose elsewhere: @@ -181,6 +196,7 @@ export OPENCLAW_DOCKER_APT_PACKAGES="ffmpeg build-essential" Notes: - This accepts a space-separated list of apt package names. +- This only works when `OPENCLAW_IMAGE=openclaw:local`. - If you change `OPENCLAW_DOCKER_APT_PACKAGES`, rerun `docker-setup.sh` to rebuild the image. diff --git a/src/docker-setup.test.ts b/src/docker-setup.test.ts index 20f754990e33..8cabe8a3f44a 100644 --- a/src/docker-setup.test.ts +++ b/src/docker-setup.test.ts @@ -25,6 +25,13 @@ if [[ "\${1:-}" == "build" ]]; then echo "build $*" >>"$log" exit 0 fi +if [[ "\${1:-}" == "pull" ]]; then + echo "pull $*" >>"$log" + if [[ "\${DOCKER_STUB_FAIL_PULL:-0}" == "1" ]]; then + exit 1 + fi + exit 0 +fi if [[ "\${1:-}" == "compose" ]]; then echo "compose $*" >>"$log" exit 0 @@ -103,6 +110,10 @@ function runDockerSetup( }); } +async function clearDockerLog(sandbox: DockerSetupSandbox) { + await writeFile(sandbox.logPath, ""); +} + function resolveBashForCompatCheck(): string | null { for (const candidate of ["/bin/bash", "bash"]) { const probe = spawnSync(candidate, ["-c", "exit 0"], { encoding: "utf8" }); @@ -131,6 +142,7 @@ describe("docker-setup.sh", () => { it("handles env defaults, home-volume mounts, and apt build args", async () => { const activeSandbox = requireSandbox(sandbox); + await clearDockerLog(activeSandbox); const result = runDockerSetup(activeSandbox, { OPENCLAW_DOCKER_APT_PACKAGES: "ffmpeg build-essential", @@ -155,6 +167,7 @@ describe("docker-setup.sh", () => { it("precreates config identity dir for CLI device auth writes", async () => { const activeSandbox = requireSandbox(sandbox); + await clearDockerLog(activeSandbox); const configDir = join(activeSandbox.rootDir, "config-identity"); const workspaceDir = join(activeSandbox.rootDir, "workspace-identity"); @@ -170,6 +183,7 @@ describe("docker-setup.sh", () => { it("rejects injected multiline OPENCLAW_EXTRA_MOUNTS values", async () => { const activeSandbox = requireSandbox(sandbox); + await clearDockerLog(activeSandbox); const result = runDockerSetup(activeSandbox, { OPENCLAW_EXTRA_MOUNTS: "/tmp:/tmp\n evil-service:\n image: alpine", @@ -181,6 +195,7 @@ describe("docker-setup.sh", () => { it("rejects invalid OPENCLAW_EXTRA_MOUNTS mount format", async () => { const activeSandbox = requireSandbox(sandbox); + await clearDockerLog(activeSandbox); const result = runDockerSetup(activeSandbox, { OPENCLAW_EXTRA_MOUNTS: "bad mount spec", @@ -192,6 +207,7 @@ describe("docker-setup.sh", () => { it("rejects invalid OPENCLAW_HOME_VOLUME names", async () => { const activeSandbox = requireSandbox(sandbox); + await clearDockerLog(activeSandbox); const result = runDockerSetup(activeSandbox, { OPENCLAW_HOME_VOLUME: "bad name", @@ -232,4 +248,44 @@ describe("docker-setup.sh", () => { expect(compose).not.toContain("gateway-daemon"); expect(compose).toContain('"gateway"'); }); + + it("pulls non-local OPENCLAW_IMAGE values instead of building locally", async () => { + const activeSandbox = requireSandbox(sandbox); + await clearDockerLog(activeSandbox); + + const result = runDockerSetup(activeSandbox, { + OPENCLAW_IMAGE: "ghcr.io/openclaw/openclaw:main", + }); + + expect(result.status).toBe(0); + const log = await readFile(activeSandbox.logPath, "utf8"); + expect(log).toContain("pull pull ghcr.io/openclaw/openclaw:main"); + expect(log).not.toContain("build build"); + }); + + it("fails fast when OPENCLAW_DOCKER_APT_PACKAGES is set with non-local OPENCLAW_IMAGE", async () => { + const activeSandbox = requireSandbox(sandbox); + await clearDockerLog(activeSandbox); + + const result = runDockerSetup(activeSandbox, { + OPENCLAW_IMAGE: "ghcr.io/openclaw/openclaw:main", + OPENCLAW_DOCKER_APT_PACKAGES: "ffmpeg", + }); + + expect(result.status).not.toBe(0); + expect(result.stderr).toContain("OPENCLAW_DOCKER_APT_PACKAGES is build-only"); + }); + + it("fails fast when docker pull fails", async () => { + const activeSandbox = requireSandbox(sandbox); + await clearDockerLog(activeSandbox); + + const result = runDockerSetup(activeSandbox, { + OPENCLAW_IMAGE: "ghcr.io/openclaw/openclaw:missing", + DOCKER_STUB_FAIL_PULL: "1", + }); + + expect(result.status).not.toBe(0); + expect(result.stderr).toContain("Failed to pull image ghcr.io/openclaw/openclaw:missing"); + }); });