mirror of
https://github.com/moltbot/moltbot.git
synced 2026-03-09 15:35:17 +00:00
* Docker: improve build cache reuse * Tests: cover Docker build cache layout * Docker: fix sandbox cache mount continuations * Docker: document qr-import manifest scope * Docker: narrow e2e install inputs * CI: cache Docker builds in workflows * CI: route sandbox smoke through setup script * CI: keep sandbox smoke on script path
231 lines
12 KiB
Docker
231 lines
12 KiB
Docker
# syntax=docker/dockerfile:1.7
|
|
|
|
# Opt-in extension dependencies at build time (space-separated directory names).
|
|
# Example: docker build --build-arg OPENCLAW_EXTENSIONS="diagnostics-otel matrix" .
|
|
#
|
|
# 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=""
|
|
ARG OPENCLAW_VARIANT=default
|
|
ARG OPENCLAW_NODE_BOOKWORM_IMAGE="node:22-bookworm@sha256:b501c082306a4f528bc4038cbf2fbb58095d583d0419a259b2114b5ac53d12e9"
|
|
ARG OPENCLAW_NODE_BOOKWORM_DIGEST="sha256:b501c082306a4f528bc4038cbf2fbb58095d583d0419a259b2114b5ac53d12e9"
|
|
ARG OPENCLAW_NODE_BOOKWORM_SLIM_IMAGE="node:22-bookworm-slim@sha256:9c2c405e3ff9b9afb2873232d24bb06367d649aa3e6259cbe314da59578e81e9"
|
|
ARG OPENCLAW_NODE_BOOKWORM_SLIM_DIGEST="sha256:9c2c405e3ff9b9afb2873232d24bb06367d649aa3e6259cbe314da59578e81e9"
|
|
|
|
# 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 multi-arch manifest list entry.
|
|
|
|
FROM ${OPENCLAW_NODE_BOOKWORM_IMAGE} 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 \
|
|
mkdir -p "/out/$ext" && \
|
|
cp "/tmp/extensions/$ext/package.json" "/out/$ext/package.json"; \
|
|
fi; \
|
|
done
|
|
|
|
# ── Stage 2: Build ──────────────────────────────────────────────
|
|
FROM ${OPENCLAW_NODE_BOOKWORM_IMAGE} AS build
|
|
|
|
# Install Bun (required for build scripts)
|
|
RUN curl -fsSL https://bun.sh/install | bash
|
|
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 --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 --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
|
|
|
|
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.
|
|
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: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
|
|
LABEL org.opencontainers.image.base.name="docker.io/library/node:22-bookworm" \
|
|
org.opencontainers.image.base.digest="${OPENCLAW_NODE_BOOKWORM_DIGEST}"
|
|
|
|
FROM ${OPENCLAW_NODE_BOOKWORM_SLIM_IMAGE} AS base-slim
|
|
ARG OPENCLAW_NODE_BOOKWORM_SLIM_DIGEST
|
|
LABEL org.opencontainers.image.base.name="docker.io/library/node:22-bookworm-slim" \
|
|
org.opencontainers.image.base.digest="${OPENCLAW_NODE_BOOKWORM_SLIM_DIGEST}"
|
|
|
|
# ── 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 --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 \
|
|
apt-get update && \
|
|
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
|
procps hostname curl git openssl
|
|
|
|
RUN chown node:node /app
|
|
|
|
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
|
|
|
|
# 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" .
|
|
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 \
|
|
apt-get update && \
|
|
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends $OPENCLAW_DOCKER_APT_PACKAGES; \
|
|
fi
|
|
|
|
# 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 node_modules COPY so playwright-core is available.
|
|
ARG OPENCLAW_INSTALL_BROWSER=""
|
|
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_BROWSER" ]; then \
|
|
apt-get update && \
|
|
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends xvfb && \
|
|
mkdir -p /home/node/.cache/ms-playwright && \
|
|
PLAYWRIGHT_BROWSERS_PATH=/home/node/.cache/ms-playwright \
|
|
node /app/node_modules/playwright-core/cli.js install --with-deps chromium && \
|
|
chown -R node:node /home/node/.cache/ms-playwright; \
|
|
fi
|
|
|
|
# Optionally install Docker CLI for sandbox container management.
|
|
# Build with: docker build --build-arg OPENCLAW_INSTALL_DOCKER_CLI=1 ...
|
|
# Adds ~50MB. Only the CLI is installed — no Docker daemon.
|
|
# Required for agents.defaults.sandbox to function in Docker deployments.
|
|
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 \
|
|
apt-get update && \
|
|
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
|
ca-certificates curl gnupg && \
|
|
install -m 0755 -d /etc/apt/keyrings && \
|
|
# Verify Docker apt signing key fingerprint before trusting it as a root key.
|
|
# Update OPENCLAW_DOCKER_GPG_FINGERPRINT when Docker rotates release keys.
|
|
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:]')" && \
|
|
actual_fingerprint="$(gpg --batch --show-keys --with-colons /tmp/docker.gpg.asc | awk -F: '$1 == "fpr" { 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
|
|
|
|
# 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
|
|
|
|
ENV NODE_ENV=production
|
|
|
|
# Security hardening: Run as non-root user
|
|
# The node:22-bookworm image includes a 'node' user (uid 1000)
|
|
# This reduces the attack surface by preventing container escape via root privileges
|
|
USER node
|
|
|
|
# Start gateway server with default config.
|
|
# Binds to loopback (127.0.0.1) by default for security.
|
|
#
|
|
# IMPORTANT: With Docker bridge networking (-p 18789:18789), loopback bind
|
|
# makes the gateway unreachable from the host. Either:
|
|
# - Use --network host, OR
|
|
# - Override --bind to "lan" (0.0.0.0) and set auth credentials
|
|
#
|
|
# Built-in probe endpoints for container health checks:
|
|
# - GET /healthz (liveness) and GET /readyz (readiness)
|
|
# - aliases: /health and /ready
|
|
# For external access from host/ingress, override bind to "lan" and set auth.
|
|
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))"
|
|
CMD ["node", "openclaw.mjs", "gateway", "--allow-unconfigured"]
|