diff --git a/.github/workflows/docker-release.yml b/.github/workflows/docker-release.yml index 7de868a9535..2cc29748c91 100644 --- a/.github/workflows/docker-release.yml +++ b/.github/workflows/docker-release.yml @@ -22,14 +22,15 @@ env: IMAGE_NAME: ${{ github.repository }} jobs: - # Build amd64 image + # Build amd64 images (default + slim share the build stage cache) build-amd64: runs-on: blacksmith-16vcpu-ubuntu-2404 permissions: packages: write contents: read outputs: - image-digest: ${{ steps.build.outputs.digest }} + digest: ${{ steps.build.outputs.digest }} + slim-digest: ${{ steps.build-slim.outputs.digest }} steps: - name: Checkout uses: actions/checkout@v4 @@ -52,12 +53,15 @@ jobs: run: | set -euo pipefail tags=() + slim_tags=() if [[ "${GITHUB_REF}" == "refs/heads/main" ]]; then tags+=("${IMAGE}:main-amd64") + slim_tags+=("${IMAGE}:main-slim-amd64") fi if [[ "${GITHUB_REF}" == refs/tags/v* ]]; then version="${GITHUB_REF#refs/tags/v}" tags+=("${IMAGE}:${version}-amd64") + slim_tags+=("${IMAGE}:${version}-slim-amd64") fi if [[ ${#tags[@]} -eq 0 ]]; then echo "::error::No amd64 tags resolved for ref ${GITHUB_REF}" @@ -68,6 +72,11 @@ jobs: printf "%s\n" "${tags[@]}" echo "EOF" } >> "$GITHUB_OUTPUT" + { + echo "slim<> "$GITHUB_OUTPUT" - name: Resolve OCI labels (amd64) id: labels @@ -101,14 +110,28 @@ jobs: provenance: false push: true - # Build arm64 image + - name: Build and push amd64 slim image + id: build-slim + uses: useblacksmith/build-push-action@v2 + with: + context: . + platforms: linux/amd64 + build-args: | + OPENCLAW_VARIANT=slim + tags: ${{ steps.tags.outputs.slim }} + labels: ${{ steps.labels.outputs.value }} + provenance: false + push: true + + # Build arm64 images (default + slim share the build stage cache) build-arm64: runs-on: blacksmith-16vcpu-ubuntu-2404-arm permissions: packages: write contents: read outputs: - image-digest: ${{ steps.build.outputs.digest }} + digest: ${{ steps.build.outputs.digest }} + slim-digest: ${{ steps.build-slim.outputs.digest }} steps: - name: Checkout uses: actions/checkout@v4 @@ -131,12 +154,15 @@ jobs: run: | set -euo pipefail tags=() + slim_tags=() if [[ "${GITHUB_REF}" == "refs/heads/main" ]]; then tags+=("${IMAGE}:main-arm64") + slim_tags+=("${IMAGE}:main-slim-arm64") fi if [[ "${GITHUB_REF}" == refs/tags/v* ]]; then version="${GITHUB_REF#refs/tags/v}" tags+=("${IMAGE}:${version}-arm64") + slim_tags+=("${IMAGE}:${version}-slim-arm64") fi if [[ ${#tags[@]} -eq 0 ]]; then echo "::error::No arm64 tags resolved for ref ${GITHUB_REF}" @@ -147,6 +173,11 @@ jobs: printf "%s\n" "${tags[@]}" echo "EOF" } >> "$GITHUB_OUTPUT" + { + echo "slim<> "$GITHUB_OUTPUT" - name: Resolve OCI labels (arm64) id: labels @@ -180,7 +211,20 @@ jobs: provenance: false push: true - # Create multi-platform manifest + - name: Build and push arm64 slim image + id: build-slim + uses: useblacksmith/build-push-action@v2 + with: + context: . + platforms: linux/arm64 + build-args: | + OPENCLAW_VARIANT=slim + tags: ${{ steps.tags.outputs.slim }} + labels: ${{ steps.labels.outputs.value }} + provenance: false + push: true + + # Create multi-platform manifests create-manifest: runs-on: blacksmith-16vcpu-ubuntu-2404 permissions: @@ -206,14 +250,18 @@ jobs: run: | set -euo pipefail tags=() + slim_tags=() if [[ "${GITHUB_REF}" == "refs/heads/main" ]]; then tags+=("${IMAGE}:main") + slim_tags+=("${IMAGE}:main-slim") fi if [[ "${GITHUB_REF}" == refs/tags/v* ]]; then version="${GITHUB_REF#refs/tags/v}" tags+=("${IMAGE}:${version}") + slim_tags+=("${IMAGE}:${version}-slim") if [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9]+)?$ ]]; then tags+=("${IMAGE}:latest") + slim_tags+=("${IMAGE}:slim") fi fi if [[ ${#tags[@]} -eq 0 ]]; then @@ -225,8 +273,13 @@ jobs: printf "%s\n" "${tags[@]}" echo "EOF" } >> "$GITHUB_OUTPUT" + { + echo "slim<> "$GITHUB_OUTPUT" - - name: Create and push manifest + - name: Create and push default manifest shell: bash run: | set -euo pipefail @@ -237,5 +290,19 @@ jobs: args+=("-t" "$tag") done docker buildx imagetools create "${args[@]}" \ - ${{ needs.build-amd64.outputs.image-digest }} \ - ${{ needs.build-arm64.outputs.image-digest }} + ${{ needs.build-amd64.outputs.digest }} \ + ${{ needs.build-arm64.outputs.digest }} + + - name: Create and push slim manifest + shell: bash + run: | + set -euo pipefail + mapfile -t tags <<< "${{ steps.tags.outputs.slim }}" + args=() + for tag in "${tags[@]}"; do + [ -z "$tag" ] && continue + args+=("-t" "$tag") + done + docker buildx imagetools create "${args[@]}" \ + ${{ needs.build-amd64.outputs.slim-digest }} \ + ${{ needs.build-arm64.outputs.slim-digest }} diff --git a/CHANGELOG.md b/CHANGELOG.md index a696f54ee54..3408e18d246 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,7 @@ Docs: https://docs.openclaw.ai - Config/Compaction safeguard tuning: expose `agents.defaults.compaction.recentTurnsPreserve` and quality-guard retry knobs through the validated config surface and embedded-runner wiring, with regression coverage for real config loading and schema metadata. (#25557) thanks @rodrigouroz. - iOS/App Store Connect release prep: align iOS bundle identifiers under `ai.openclaw.client`, refresh Watch app icons, add Fastlane metadata/screenshot automation, and support Keychain-backed ASC auth for uploads. (#38936) Thanks @ngutman. - Mattermost/model picker: add Telegram-style interactive provider/model browsing for `/oc_model` and `/oc_models`, fix picker callback updates, and emit a normal confirmation reply when a model is selected. (#38767) thanks @mukhtharcm. +- Docker/multi-stage build: restructure Dockerfile as a multi-stage build to produce a minimal runtime image without build tools, source code, or Bun; add `OPENCLAW_VARIANT=slim` build arg for a bookworm-slim variant. (#38479) Thanks @sallyom. ### Breaking diff --git a/Dockerfile b/Dockerfile index 3b51860cf6b..a4a98e305e1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,14 +1,27 @@ # 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. +# Multi-stage build produces a minimal runtime image without build tools, +# source code, or Bun. Works with Docker, Buildx, and Podman. +# The ext-deps stage extracts only the package.json files we need from +# extensions/, so the main build layer is not invalidated by unrelated +# extension source changes. +# +# Two runtime variants: +# Default (bookworm): docker build . +# Slim (bookworm-slim): docker build --build-arg OPENCLAW_VARIANT=slim . ARG OPENCLAW_EXTENSIONS="" -FROM node:22-bookworm@sha256:cd7bcd2e7a1e6f72052feb023c7f6b722205d3fcab7bbcbd2d1bfdab10b1e935 AS ext-deps +ARG OPENCLAW_VARIANT=default + +# Base images are pinned to SHA256 digests for reproducible builds. +# Trade-off: digests must be updated manually when upstream tags move. +# To update, run: docker manifest inspect node:22-bookworm (or podman) +# and replace the digest below with the current amd64 entry. + +FROM node:22-bookworm@sha256:6d735b4d33660225271fda0a412802746658c3a1b975507b2803ed299609760a AS ext-deps ARG OPENCLAW_EXTENSIONS COPY extensions /tmp/extensions +# Copy package.json for opted-in extensions so pnpm resolves their deps. RUN mkdir -p /out && \ for ext in $OPENCLAW_EXTENSIONS; do \ if [ -f "/tmp/extensions/$ext/package.json" ]; then \ @@ -17,20 +30,8 @@ RUN mkdir -p /out && \ fi; \ done -FROM node:22-bookworm@sha256:cd7bcd2e7a1e6f72052feb023c7f6b722205d3fcab7bbcbd2d1bfdab10b1e935 - -# OCI base-image metadata for downstream image consumers. -# If you change these annotations, also update: -# - docs/install/docker.md ("Base image metadata" section) -# - https://docs.openclaw.ai/install/docker -LABEL org.opencontainers.image.base.name="docker.io/library/node:22-bookworm" \ - org.opencontainers.image.base.digest="sha256:cd7bcd2e7a1e6f72052feb023c7f6b722205d3fcab7bbcbd2d1bfdab10b1e935" \ - org.opencontainers.image.source="https://github.com/openclaw/openclaw" \ - org.opencontainers.image.url="https://openclaw.ai" \ - org.opencontainers.image.documentation="https://docs.openclaw.ai/install/docker" \ - org.opencontainers.image.licenses="MIT" \ - org.opencontainers.image.title="OpenClaw" \ - org.opencontainers.image.description="OpenClaw gateway and CLI runtime container image" +# ── Stage 2: Build ────────────────────────────────────────────── +FROM node:22-bookworm@sha256:6d735b4d33660225271fda0a412802746658c3a1b975507b2803ed299609760a AS build # Install Bun (required for build scripts) RUN curl -fsSL https://bun.sh/install | bash @@ -39,8 +40,80 @@ ENV PATH="/root/.bun/bin:${PATH}" RUN corepack enable WORKDIR /app + +COPY package.json pnpm-lock.yaml pnpm-workspace.yaml .npmrc ./ +COPY ui/package.json ./ui/package.json +COPY patches ./patches +COPY scripts ./scripts + +COPY --from=ext-deps /out/ ./extensions/ + +# Reduce OOM risk on low-memory hosts during dependency installation. +# Docker builds on small VMs may otherwise fail with "Killed" (exit 137). +RUN NODE_OPTIONS=--max-old-space-size=2048 pnpm install --frozen-lockfile + +COPY . . + +# 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. +RUN pnpm canvas:a2ui:bundle || \ + (echo "A2UI bundle: creating stub (non-fatal)" && \ + mkdir -p src/canvas-host/a2ui && \ + 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 +# Force pnpm for UI build (Bun may fail on ARM/Synology architectures) +ENV OPENCLAW_PREFER_PNPM=1 +RUN pnpm ui:build + +# ── Runtime base images ───────────────────────────────────────── +FROM node:22-bookworm@sha256:6d735b4d33660225271fda0a412802746658c3a1b975507b2803ed299609760a AS base-default +LABEL org.opencontainers.image.base.name="docker.io/library/node:22-bookworm" \ + org.opencontainers.image.base.digest="sha256:6d735b4d33660225271fda0a412802746658c3a1b975507b2803ed299609760a" + +FROM node:22-bookworm-slim@sha256:b41c15b715b5d6e3f305e9c6480a2396dd5f130b63add98d3d45760376f20823 AS base-slim +LABEL org.opencontainers.image.base.name="docker.io/library/node:22-bookworm-slim" \ + org.opencontainers.image.base.digest="sha256:b41c15b715b5d6e3f305e9c6480a2396dd5f130b63add98d3d45760376f20823" + +# ── Stage 3: Runtime ──────────────────────────────────────────── +FROM base-${OPENCLAW_VARIANT} +ARG OPENCLAW_VARIANT + +# OCI base-image metadata for downstream image consumers. +# If you change these annotations, also update: +# - docs/install/docker.md ("Base image metadata" section) +# - https://docs.openclaw.ai/install/docker +LABEL org.opencontainers.image.source="https://github.com/openclaw/openclaw" \ + org.opencontainers.image.url="https://openclaw.ai" \ + org.opencontainers.image.documentation="https://docs.openclaw.ai/install/docker" \ + org.opencontainers.image.licenses="MIT" \ + org.opencontainers.image.title="OpenClaw" \ + org.opencontainers.image.description="OpenClaw gateway and CLI runtime container image" + +WORKDIR /app + +# Install system utilities present in bookworm but missing in bookworm-slim. +# On the full bookworm image these are already installed (apt-get is a no-op). +RUN apt-get update && \ + DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + procps hostname curl git openssl && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/* + 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 + +# Install additional system packages needed by your skills or extensions. +# Example: docker build --build-arg OPENCLAW_DOCKER_APT_PACKAGES="python3 wget" . ARG OPENCLAW_DOCKER_APT_PACKAGES="" RUN if [ -n "$OPENCLAW_DOCKER_APT_PACKAGES" ]; then \ apt-get update && \ @@ -49,23 +122,10 @@ RUN if [ -n "$OPENCLAW_DOCKER_APT_PACKAGES" ]; then \ rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/*; \ fi -COPY --chown=node:node package.json pnpm-lock.yaml pnpm-workspace.yaml .npmrc ./ -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). -RUN NODE_OPTIONS=--max-old-space-size=2048 pnpm install --frozen-lockfile - # Optionally install Chromium and Xvfb for browser automation. # Build with: docker build --build-arg OPENCLAW_INSTALL_BROWSER=1 ... # Adds ~300MB but eliminates the 60-90s Playwright install on every container start. -# Must run after pnpm install so playwright-core is available in node_modules. -USER root +# Must run after node_modules COPY so playwright-core is available. ARG OPENCLAW_INSTALL_BROWSER="" RUN if [ -n "$OPENCLAW_INSTALL_BROWSER" ]; then \ apt-get update && \ @@ -110,9 +170,7 @@ RUN if [ -n "$OPENCLAW_INSTALL_DOCKER_CLI" ]; then \ rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/*; \ fi -USER node -COPY --chown=node:node . . -# Normalize copied plugin/agent paths so plugin safety checks do not reject +# 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 \ @@ -120,13 +178,8 @@ RUN for dir in /app/extensions /app/.agent /app/.agents; do \ find "$dir" -type f -exec chmod 644 {} +; \ fi; \ done -RUN pnpm build -# Force pnpm for UI build (Bun may fail on ARM/Synology architectures) -ENV OPENCLAW_PREFER_PNPM=1 -RUN pnpm ui:build # Expose the CLI binary without requiring npm global writes as non-root. -USER root RUN ln -sf /app/openclaw.mjs /usr/local/bin/openclaw \ && chmod 755 /app/openclaw.mjs diff --git a/docs/install/docker.md b/docs/install/docker.md index 0eeacd63ffe..b3d3daf798d 100644 --- a/docs/install/docker.md +++ b/docs/install/docker.md @@ -170,7 +170,7 @@ The main Docker image currently uses: The docker image now publishes OCI base-image annotations (sha256 is an example): - `org.opencontainers.image.base.name=docker.io/library/node:22-bookworm` -- `org.opencontainers.image.base.digest=sha256:cd7bcd2e7a1e6f72052feb023c7f6b722205d3fcab7bbcbd2d1bfdab10b1e935` +- `org.opencontainers.image.base.digest=sha256:6d735b4d33660225271fda0a412802746658c3a1b975507b2803ed299609760a` - `org.opencontainers.image.source=https://github.com/openclaw/openclaw` - `org.opencontainers.image.url=https://openclaw.ai` - `org.opencontainers.image.documentation=https://docs.openclaw.ai/install/docker`