Docker: trim runtime image payload (#40307)

* Docker: shrink runtime image payload

* Docker: add runtime pnpm opt-in

* Docker: collapse helper entrypoint chmod layers

* Docker: restore bundled pnpm runtime

* Update CHANGELOG.md
This commit is contained in:
Vincent Koc 2026-03-08 16:07:04 -07:00 committed by GitHub
parent bd1fe4d8b4
commit 3f3f66a5f7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 56 additions and 34 deletions

View File

@ -9,6 +9,7 @@ Docs: https://docs.openclaw.ai
### Breaking
### Fixes
- Docker/runtime image: prune dev dependencies, strip build-only dist metadata for smaller Docker images. (#40307) Thanks @vincentkoc.
## 2026.3.8
@ -46,7 +47,6 @@ Docs: https://docs.openclaw.ai
- Context engine registry/bundled builds: share the registry state through a `globalThis` singleton so duplicated bundled module copies can resolve engines registered by each other at runtime, with regression coverage for duplicate-module imports. (#40115) thanks @jalehman.
- macOS/Tailscale gateway discovery: keep Tailscale Serve probing alive when other remote gateways are already discovered, prefer direct transport for resolved `.ts.net` and Tailscale Serve gateways, and set `TERM=dumb` for GUI-launched Tailscale CLI discovery. (#40167) thanks @ngutman.
- Podman/setup: fix `cannot chdir: Permission denied` in `run_as_user` when `setup-podman.sh` is invoked from a directory the target user cannot access, by wrapping user-switch calls in a subshell that cd's to `/tmp` with `/` fallback. (#39435) Thanks @langdon and @jlcbk.
- Podman/SELinux: auto-detect SELinux enforcing/permissive mode and add `:Z` relabel to bind mounts in `run-openclaw-podman.sh` and the Quadlet template, fixing `EACCES` on Fedora/RHEL hosts. Supports `OPENCLAW_BIND_MOUNT_OPTIONS` override. (#39449) Thanks @langdon and @githubbzxs.
## 2026.3.7

View File

@ -58,6 +58,15 @@ RUN NODE_OPTIONS=--max-old-space-size=2048 pnpm install --frozen-lockfile
COPY . .
# Normalize extension paths now so runtime COPY preserves safe modes
# without adding a second full extensions layer.
RUN for dir in /app/extensions /app/.agent /app/.agents; do \
if [ -d "$dir" ]; then \
find "$dir" -type d -exec chmod 755 {} +; \
find "$dir" -type f -exec chmod 644 {} +; \
fi; \
done
# A2UI bundle may fail under QEMU cross-compilation (e.g. building amd64
# on Apple Silicon). CI builds natively per-arch so this is a no-op there.
# Stub it so local cross-arch builds still succeed.
@ -67,11 +76,17 @@ RUN pnpm canvas:a2ui:bundle || \
echo "/* A2UI bundle unavailable in this build */" > src/canvas-host/a2ui/a2ui.bundle.js && \
echo "stub" > src/canvas-host/a2ui/.bundle.hash && \
rm -rf vendor/a2ui apps/shared/OpenClawKit/Tools/CanvasA2UI)
RUN pnpm build
RUN pnpm build:docker
# Force pnpm for UI build (Bun may fail on ARM/Synology architectures)
ENV OPENCLAW_PREFER_PNPM=1
RUN pnpm ui:build
# Prune dev dependencies and strip build-only metadata before copying
# runtime assets into the final image.
FROM build AS runtime-assets
RUN CI=true pnpm prune --prod && \
find dist -type f \( -name '*.d.ts' -o -name '*.d.mts' -o -name '*.d.cts' -o -name '*.map' \) -delete
# ── Runtime base images ─────────────────────────────────────────
FROM ${OPENCLAW_NODE_BOOKWORM_IMAGE} AS base-default
ARG OPENCLAW_NODE_BOOKWORM_DIGEST
@ -110,19 +125,22 @@ RUN apt-get update && \
RUN chown node:node /app
COPY --from=build --chown=node:node /app/dist ./dist
COPY --from=build --chown=node:node /app/node_modules ./node_modules
COPY --from=build --chown=node:node /app/package.json .
COPY --from=build --chown=node:node /app/openclaw.mjs .
COPY --from=build --chown=node:node /app/extensions ./extensions
COPY --from=build --chown=node:node /app/skills ./skills
COPY --from=build --chown=node:node /app/docs ./docs
COPY --from=runtime-assets --chown=node:node /app/dist ./dist
COPY --from=runtime-assets --chown=node:node /app/node_modules ./node_modules
COPY --from=runtime-assets --chown=node:node /app/package.json .
COPY --from=runtime-assets --chown=node:node /app/openclaw.mjs .
COPY --from=runtime-assets --chown=node:node /app/extensions ./extensions
COPY --from=runtime-assets --chown=node:node /app/skills ./skills
COPY --from=runtime-assets --chown=node:node /app/docs ./docs
# Docker live-test runners invoke `pnpm` inside the runtime image.
# Activate the exact pinned package manager now so the container does not
# rely on a first-run network fetch or missing shims under the non-root user.
RUN corepack enable && \
corepack prepare "$(node -p "require('./package.json').packageManager")" --activate
# Keep pnpm available in the runtime image for container-local workflows.
# Use a shared Corepack home so the non-root `node` user does not need a
# first-run network fetch when invoking pnpm.
ENV COREPACK_HOME=/usr/local/share/corepack
RUN install -d -m 0755 "$COREPACK_HOME" && \
corepack enable && \
corepack prepare "$(node -p "require('./package.json').packageManager")" --activate && \
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" .
@ -182,15 +200,6 @@ RUN if [ -n "$OPENCLAW_INSTALL_DOCKER_CLI" ]; then \
rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/*; \
fi
# Normalize extension paths so plugin safety checks do not reject
# world-writable directories inherited from source file modes.
RUN for dir in /app/extensions /app/.agent /app/.agents; do \
if [ -d "$dir" ]; then \
find "$dir" -type d -exec chmod 755 {} +; \
find "$dir" -type f -exec chmod 644 {} +; \
fi; \
done
# Expose the CLI binary without requiring npm global writes as non-root.
RUN ln -sf /app/openclaw.mjs /usr/local/bin/openclaw \
&& chmod 755 /app/openclaw.mjs

View File

@ -20,8 +20,7 @@ RUN apt-get update \
xvfb \
&& rm -rf /var/lib/apt/lists/*
COPY scripts/sandbox-browser-entrypoint.sh /usr/local/bin/openclaw-sandbox-browser
RUN chmod +x /usr/local/bin/openclaw-sandbox-browser
COPY --chmod=755 scripts/sandbox-browser-entrypoint.sh /usr/local/bin/openclaw-sandbox-browser
RUN useradd --create-home --shell /bin/bash sandbox
USER sandbox

View File

@ -224,6 +224,7 @@
"android:test": "cd apps/android && ./gradlew :app:testDebugUnitTest",
"android:test:integration": "OPENCLAW_LIVE_TEST=1 OPENCLAW_LIVE_ANDROID_NODE=1 vitest run --config vitest.live.config.ts src/gateway/android-node.capabilities.live.test.ts",
"build": "pnpm canvas:a2ui:bundle && node scripts/tsdown-build.mjs && node scripts/copy-plugin-sdk-root-alias.mjs && pnpm build:plugin-sdk:dts && node --import tsx scripts/write-plugin-sdk-entry-dts.ts && node --import tsx scripts/canvas-a2ui-copy.ts && node --import tsx scripts/copy-hook-metadata.ts && node --import tsx scripts/copy-export-html-templates.ts && node --import tsx scripts/write-build-info.ts && node --import tsx scripts/write-cli-startup-metadata.ts && node --import tsx scripts/write-cli-compat.ts",
"build:docker": "node scripts/tsdown-build.mjs && node scripts/copy-plugin-sdk-root-alias.mjs && pnpm build:plugin-sdk:dts && node --import tsx scripts/write-plugin-sdk-entry-dts.ts && node --import tsx scripts/canvas-a2ui-copy.ts && node --import tsx scripts/copy-hook-metadata.ts && node --import tsx scripts/copy-export-html-templates.ts && node --import tsx scripts/write-build-info.ts && node --import tsx scripts/write-cli-startup-metadata.ts && node --import tsx scripts/write-cli-compat.ts",
"build:plugin-sdk:dts": "tsc -p tsconfig.plugin-sdk.dts.json",
"build:strict-smoke": "pnpm canvas:a2ui:bundle && node scripts/tsdown-build.mjs && node scripts/copy-plugin-sdk-root-alias.mjs && pnpm build:plugin-sdk:dts",
"canvas:a2ui:bundle": "bash scripts/bundle-a2ui.sh",

View File

@ -13,7 +13,6 @@ RUN corepack enable \
&& pnpm install --frozen-lockfile
COPY . .
COPY scripts/docker/cleanup-smoke/run.sh /usr/local/bin/openclaw-cleanup-smoke
RUN chmod +x /usr/local/bin/openclaw-cleanup-smoke
COPY --chmod=755 scripts/docker/cleanup-smoke/run.sh /usr/local/bin/openclaw-cleanup-smoke
ENTRYPOINT ["/usr/local/bin/openclaw-cleanup-smoke"]

View File

@ -9,8 +9,7 @@ RUN apt-get update \
&& rm -rf /var/lib/apt/lists/*
COPY install-sh-common/version-parse.sh /usr/local/install-sh-common/version-parse.sh
COPY run.sh /usr/local/bin/openclaw-install-e2e
RUN chmod +x /usr/local/bin/openclaw-install-e2e
COPY --chmod=755 run.sh /usr/local/bin/openclaw-install-e2e
RUN useradd --create-home --shell /bin/bash appuser
USER appuser

View File

@ -28,7 +28,6 @@ ENV NPM_CONFIG_AUDIT=false
COPY install-sh-common/cli-verify.sh /usr/local/install-sh-common/cli-verify.sh
COPY install-sh-common/version-parse.sh /usr/local/install-sh-common/version-parse.sh
COPY install-sh-nonroot/run.sh /usr/local/bin/openclaw-install-nonroot
RUN sudo chmod +x /usr/local/bin/openclaw-install-nonroot
COPY --chmod=755 install-sh-nonroot/run.sh /usr/local/bin/openclaw-install-nonroot
ENTRYPOINT ["/usr/local/bin/openclaw-install-nonroot"]

View File

@ -20,7 +20,6 @@ RUN set -eux; \
COPY install-sh-common/cli-verify.sh /usr/local/install-sh-common/cli-verify.sh
COPY install-sh-common/version-parse.sh /usr/local/install-sh-common/version-parse.sh
COPY install-sh-smoke/run.sh /usr/local/bin/openclaw-install-smoke
RUN chmod +x /usr/local/bin/openclaw-install-smoke
COPY --chmod=755 install-sh-smoke/run.sh /usr/local/bin/openclaw-install-smoke
ENTRYPOINT ["/usr/local/bin/openclaw-install-smoke"]

View File

@ -163,7 +163,7 @@ describe("docker-setup.sh", () => {
sandbox = null;
});
it("handles env defaults, home-volume mounts, and apt build args", async () => {
it("handles env defaults, home-volume mounts, and Docker build args", async () => {
const activeSandbox = requireSandbox(sandbox);
const result = runDockerSetup(activeSandbox, {

View File

@ -37,6 +37,15 @@ describe("Dockerfile", () => {
expect(dockerfile).toContain("apt-get install -y --no-install-recommends xvfb");
});
it("prunes runtime dependencies after the build stage", async () => {
const dockerfile = await readFile(dockerfilePath, "utf8");
expect(dockerfile).toContain("FROM build AS runtime-assets");
expect(dockerfile).toContain("CI=true pnpm prune --prod");
expect(dockerfile).toContain(
"COPY --from=runtime-assets --chown=node:node /app/node_modules ./node_modules",
);
});
it("normalizes plugin and agent paths permissions in image layers", async () => {
const dockerfile = await readFile(dockerfilePath, "utf8");
expect(dockerfile).toContain("for dir in /app/extensions /app/.agent /app/.agents");
@ -49,4 +58,12 @@ describe("Dockerfile", () => {
expect(dockerfile).toContain('== "fpr" {');
expect(dockerfile).not.toContain('\\"fpr\\"');
});
it("keeps runtime pnpm available", async () => {
const dockerfile = await readFile(dockerfilePath, "utf8");
expect(dockerfile).toContain("ENV COREPACK_HOME=/usr/local/share/corepack");
expect(dockerfile).toContain(
'corepack prepare "$(node -p "require(\'./package.json\').packageManager")" --activate',
);
});
});