# =============================================================================
# OpenClaw App Image - Layer 2(继承 Layer 1 + 执行官方构建)
# =============================================================================
# 功能:
# - 继承 Layer 1 (openclaw-base) 的基础设施
# - 执行 OpenClaw 源码构建(pnpm install + build)
#
# 构建参数:
# OPENCLAW_VERSION → OpenClaw 版本
# OPENCLAW_EXTENSIONS → 扩展插件列表(空格分隔)
# NPM_REGISTRY → npm 镜像地址
#
# 构建上下文:
# 必须在 OpenClaw 源码根目录执行(即包含 packages/ 和 extensions/ 的目录),
# --mount=type=bind 的 source 路径(packages、${OPENCLAW_BUNDLED_PLUGIN_DIR})
# 均相对于构建上下文解析,从 docker/ 目录构建会导致这些路径解析失败。
#
# 构建顺序:
# 1. cd <openclaw-src> && docker build -t openclaw:base-X.Y.Z -f docker/Dockerfile.openclaw-base .
# 2. docker build -t openclaw:app-X.Y.Z -f docker/Dockerfile.openclaw-app . --build-arg OPENCLAW_VERSION=X.Y.Z
# 3. docker build -t openclaw:X.Y.Z -f docker/Dockerfile.openclaw-overlay docker/
# =============================================================================
ARG OPENCLAW_BASE_IMAGE=openclaw:base-2026.5.22
FROM ${OPENCLAW_BASE_IMAGE}
LABEL org.opencontainers.image.title="OpenClaw App"
LABEL org.opencontainers.image.description="OpenClaw with build tools"
LABEL org.openclaw.layer="app"
LABEL org.openclaw.version="${OPENCLAW_VERSION:-2026.5.22}"
ARG OPENCLAW_EXTENSIONS=""
ARG OPENCLAW_BUNDLED_PLUGIN_DIR=extensions
FROM ${OPENCLAW_BASE_IMAGE} AS workspace-deps
ARG OPENCLAW_EXTENSIONS
ARG OPENCLAW_BUNDLED_PLUGIN_DIR=extensions
USER root
# Copy package.json files for workspace packages used by the install layer.
RUN --mount=type=bind,source=packages,target=/tmp/packages,readonly \
--mount=type=bind,source=${OPENCLAW_BUNDLED_PLUGIN_DIR},target=/tmp/${OPENCLAW_BUNDLED_PLUGIN_DIR},readonly \
mkdir -p /out/packages "/out/${OPENCLAW_BUNDLED_PLUGIN_DIR}" && \
for manifest in /tmp/packages/*/package.json; do \
[ -f "$manifest" ] || continue; \
pkg_dir="${manifest%/package.json}"; \
pkg_name="${pkg_dir##*/}"; \
mkdir -p "/out/packages/$pkg_name" && \
cp "$manifest" "/out/packages/$pkg_name/package.json"; \
done && \
for ext in $(printf '%s\n' "$OPENCLAW_EXTENSIONS" | tr ',' ' '); do \
if [ -f "/tmp/${OPENCLAW_BUNDLED_PLUGIN_DIR}/$ext/package.json" ]; then \
mkdir -p "/out/${OPENCLAW_BUNDLED_PLUGIN_DIR}/$ext" && \
cp "/tmp/${OPENCLAW_BUNDLED_PLUGIN_DIR}/$ext/package.json" "/out/${OPENCLAW_BUNDLED_PLUGIN_DIR}/$ext/package.json"; \
fi; \
done
FROM ${OPENCLAW_BASE_IMAGE} AS build
ARG OPENCLAW_BUNDLED_PLUGIN_DIR=extensions
USER root
# base 的 /root/.npmrc 预设了 registry=https://registry.npmmirror.com,此处覆盖为官方源
ARG NPM_REGISTRY="https://registry.npmjs.org/"
RUN npm config set registry ${NPM_REGISTRY}
# pnpm 超时/重试配置(大文件下载易超时)
RUN pnpm config set fetch-timeout 300000 && \
pnpm config set fetch-retries 5 && \
pnpm config set fetch-retry-mintimeout 20000 && \
pnpm config set fetch-retry-maxtimeout 120000
WORKDIR /app
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml .npmrc ./
COPY openclaw.mjs ./
COPY ui/package.json ./ui/package.json
COPY patches ./patches
COPY scripts/postinstall-bundled-plugins.mjs scripts/preinstall-package-manager-warning.mjs scripts/npm-runner.mjs scripts/windows-cmd-helpers.mjs ./scripts/
COPY scripts/lib/package-dist-imports.mjs ./scripts/lib/package-dist-imports.mjs
COPY --from=workspace-deps /out/packages/ ./packages/
COPY --from=workspace-deps /out/${OPENCLAW_BUNDLED_PLUGIN_DIR}/ ./${OPENCLAW_BUNDLED_PLUGIN_DIR}/
# pnpm install 触发 prepare 生命周期,Docker 构建中 git hooks 无意义,创建空脚本绕过
RUN echo 'process.exit(0)' > /app/scripts/prepare-git-hooks.mjs
RUN --mount=type=cache,id=openclaw-pnpm-store,target=/root/.local/share/pnpm/store,sharing=locked \
NODE_OPTIONS=--max-old-space-size=2048 \
pnpm install --frozen-lockfile \
--config.supportedArchitectures.os=linux \
--config.supportedArchitectures.cpu="$(node -p 'process.arch')" \
--config.supportedArchitectures.libc=glibc
RUN set -eux; \
echo "==> Verifying critical native addons..."; \
for attempt in 1 2 3 4 5; do \
if find /app/node_modules -name "matrix-sdk-crypto*.node" 2>/dev/null | grep -q .; then \
exit 0; \
fi; \
echo "matrix-sdk-crypto native addon missing; retrying download (${attempt}/5)"; \
node /app/node_modules/@matrix-org/matrix-sdk-crypto-nodejs/download-lib.js || true; \
sleep $((attempt * 2)); \
done; \
find /app/node_modules -name "matrix-sdk-crypto*.node" 2>/dev/null | grep -q . || \
(echo "ERROR: matrix-sdk-crypto native addon missing after retries" >&2 && exit 1)
COPY . .
# 修复二进制/脚本可执行权限(构建上下文已排除 node_modules,无需备份)
RUN find /app/node_modules/.bin -type f -exec chmod +x {} + 2>/dev/null || true; \
find /app/node_modules/.bin -type l -exec chmod +x {} + 2>/dev/null || true; \
find /app/node_modules -type f -name 'esbuild' -exec chmod +x {} + 2>/dev/null || true; \
find /app/node_modules -type f -name '*.node' -exec chmod +x {} + 2>/dev/null || true;
# 规范化扩展目录权限,避免运行时 COPY 添加额外层
RUN for dir in /app/${OPENCLAW_BUNDLED_PLUGIN_DIR} /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
RUN pnpm_config_verify_deps_before_run=false pnpm canvas:a2ui:bundle || \
(echo "A2UI bundle: creating stub (non-fatal)" && \
mkdir -p extensions/canvas/src/host/a2ui && \
echo "/* A2UI bundle unavailable in this build */" > extensions/canvas/src/host/a2ui/a2ui.bundle.js && \
echo "stub" > extensions/canvas/src/host/a2ui/.bundle.hash && \
rm -rf vendor/a2ui apps/shared/OpenClawKit/Tools/CanvasA2UI)
# build:docker 前修复可执行权限(canvas:a2ui:bundle 可能丢失 +x)
RUN find /app/node_modules -path '*/node_modules/.bin/*' -type f -exec chmod +x {} + 2>/dev/null; \
find /app/node_modules -path '*/node_modules/.bin/*' -type l -exec chmod +x {} + 2>/dev/null || true; \
find /app/node_modules -type f -name 'esbuild' -exec chmod +x {} + 2>/dev/null || true; \
find /app/node_modules -type f -name '*.node' -exec chmod +x {} + 2>/dev/null || true; \
NODE_OPTIONS=--max-old-space-size=8192 pnpm_config_verify_deps_before_run=false pnpm build:docker
ENV OPENCLAW_PREFER_PNPM=1
RUN pnpm_config_verify_deps_before_run=false pnpm ui:build
RUN pnpm_config_verify_deps_before_run=false pnpm qa:lab:build
FROM build AS runtime-assets
ARG OPENCLAW_EXTENSIONS
ARG OPENCLAW_BUNDLED_PLUGIN_DIR=extensions
# 跳过 pnpm prune --prod(离线环境下不可靠),直接使用完整 node_modules
RUN echo "cache-bust: skip-pnpm-prune 20260609" && \
OPENCLAW_EXTENSIONS="$OPENCLAW_EXTENSIONS" OPENCLAW_BUNDLED_PLUGIN_DIR="$OPENCLAW_BUNDLED_PLUGIN_DIR" node scripts/prune-docker-plugin-dist.mjs && \
node scripts/postinstall-bundled-plugins.mjs && \
find dist -type f \( -name '*.d.ts' -o -name '*.d.mts' -o -name '*.d.cts' -o -name '*.map' \) -delete && \
node scripts/check-package-dist-imports.mjs /app
FROM ${OPENCLAW_BASE_IMAGE} AS base-runtime
USER root
LABEL org.opencontainers.image.base.name="openclaw-base" \
org.opencontainers.image.description="Based on openclaw-base with Playwright/bun/pnpm/system tools"
FROM base-runtime
ARG OPENCLAW_BUNDLED_PLUGIN_DIR=extensions
USER root
# OCI base-image metadata for downstream image consumers.
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
# openclaw-base 已包含大部分系统包,仅安装缺失的 tini 和 ca-certificates
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 \
mkdir -p /var/lib/apt/lists/partial && \
apt-get update && \
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
ca-certificates tini && \
update-ca-certificates
RUN chown node:node /app
COPY --from=runtime-assets --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/pnpm-workspace.yaml .
COPY --from=build --chown=node:node /app/patches ./patches
COPY --from=build --chown=node:node /app/openclaw.mjs .
COPY --from=build --chown=node:node /app/${OPENCLAW_BUNDLED_PLUGIN_DIR} ./${OPENCLAW_BUNDLED_PLUGIN_DIR}
COPY --from=build --chown=node:node /app/skills ./skills
COPY --from=build --chown=node:node /app/docs ./docs
COPY --from=build --chown=node:node /app/qa ./qa
# 复制 workspace 模板到 /app/src/agents/templates/(gateway 运行时需要)
RUN mkdir -p /app/src/agents/templates && \
if [ -d /app/node_modules/openclaw/src/agents/templates ]; then \
cp /app/node_modules/openclaw/src/agents/templates/* /app/src/agents/templates/ 2>/dev/null || true; \
fi && \
if [ -d /app/node_modules/openclaw/docs/reference/templates ]; then \
cp -n /app/node_modules/openclaw/docs/reference/templates/* /app/src/agents/templates/ 2>/dev/null || true; \
fi && \
if [ -d /app/docs/reference/templates ]; then \
cp -n /app/docs/reference/templates/* /app/src/agents/templates/ 2>/dev/null || true; \
fi && \
chown -R node:node /app/src
# 使用 corepack 确保 pnpm 版本与 packageManager 字段一致
ENV COREPACK_HOME=/usr/local/share/corepack
RUN install -d -m 0755 "$COREPACK_HOME" && \
corepack enable && \
for attempt in 1 2 3 4 5; do \
if corepack prepare "$(node -p "require('./package.json').packageManager")" --activate; then \
break; \
fi; \
if [ "$attempt" -eq 5 ]; then \
exit 1; \
fi; \
sleep $((attempt * 2)); \
done && \
chmod -R a+rX "$COREPACK_HOME"
# 可选 APT 包安装
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 \
packages="${OPENCLAW_IMAGE_APT_PACKAGES-$OPENCLAW_DOCKER_APT_PACKAGES}"; \
if [ -n "$packages" ]; then \
mkdir -p /var/lib/apt/lists/partial && \
apt-get update && \
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends $packages; \
fi
# 可选 PIP 包安装(openclaw-base 已包含基础 pip 包)
ARG OPENCLAW_IMAGE_PIP_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_IMAGE_PIP_PACKAGES" ]; then \
if ! python3 -m pip --version >/dev/null 2>&1; then \
mkdir -p /var/lib/apt/lists/partial && \
apt-get update && \
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends python3-pip; \
fi && \
python3 -m pip install --no-cache-dir --break-system-packages $OPENCLAW_IMAGE_PIP_PACKAGES; \
fi
ENV PLAYWRIGHT_BROWSERS_PATH=/home/node/.cache/ms-playwright
# 可选 Docker CLI 安装(sandbox 容器管理需要,增加约 50MB)
ARG OPENCLAW_INSTALL_DOCKER_CLI=""
ARG OPENCLAW_DOCKER_GPG_FINGERPRINT="9DC858229FC7DD38854AE2D88D81803C0EBFCD88"
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_INSTALL_DOCKER_CLI" ]; then \
mkdir -p /var/lib/apt/lists/partial && \
apt-get update && \
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
ca-certificates curl gnupg && \
install -m 0755 -d /etc/apt/keyrings && \
curl -fsSL https://download.docker.com/linux/debian/gpg -o /tmp/docker.gpg.asc && \
expected_fingerprint="$(printf '%s' "$OPENCLAW_DOCKER_GPG_FINGERPRINT" | tr '[:lower:]' '[:upper:]' | tr -d '[:space:]')" && \
docker_gpg_pub_count="$(gpg --batch --show-keys --with-colons /tmp/docker.gpg.asc | awk -F: '$1 == "pub" { c++ } END { print c+0 }')" && \
if [ "$docker_gpg_pub_count" != "1" ]; then \
echo "ERROR: Docker apt key must contain exactly one public key (found $docker_gpg_pub_count); refusing a multi-key file." >&2; \
exit 1; \
fi && \
actual_fingerprint="$(gpg --batch --show-keys --with-colons /tmp/docker.gpg.asc | awk -F: 'BEGIN{f="f";p="p";r="r";t=sprintf("%s%s%s",f,p,r)}$1==t{print toupper($10);exit}')" && \
if [ -z "$actual_fingerprint" ] || [ "$actual_fingerprint" != "$expected_fingerprint" ]; then \
echo "ERROR: Docker apt key fingerprint mismatch (expected $expected_fingerprint, got ${actual_fingerprint:-<empty>})" >&2; \
exit 1; \
fi && \
gpg --dearmor -o /etc/apt/keyrings/docker.gpg /tmp/docker.gpg.asc && \
rm -f /tmp/docker.gpg.asc && \
chmod a+r /etc/apt/keyrings/docker.gpg && \
printf 'deb [arch=%s signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian bookworm stable\n' \
"$(dpkg --print-architecture)" > /etc/apt/sources.list.d/docker.list && \
apt-get update && \
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
docker-ce-cli docker-compose-plugin; \
fi
RUN ln -sf /app/openclaw.mjs /usr/local/bin/openclaw \
&& chmod 755 /app/openclaw.mjs
# 预创建 volume 挂载点,确保目录属主为 node
RUN install -d -m 0700 -o node -g node \
/home/node/.openclaw \
/home/node/.openclaw/workspace \
/home/node/.config/openclaw && \
stat -c '%U:%G %a' /home/node/.openclaw | grep -qx 'node:node 700' && \
stat -c '%U:%G %a' /home/node/.openclaw/workspace | grep -qx 'node:node 700' && \
stat -c '%U:%G %a' /home/node/.config/openclaw | grep -qx 'node:node 700'
ENV NODE_ENV=production
USER node
# 网关入口:绑定 127.0.0.1:18789,需 --network host 或覆盖 --bind lan 方可从外部访问
HEALTHCHECK --interval=3m --timeout=10s --start-period=15s --retries=3 \
CMD node -e "fetch('http://127.0.0.1:18789/healthz').then((r)=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"
ENTRYPOINT ["tini", "-s", "--"]
CMD ["node", "openclaw.mjs", "gateway"]