-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathDockerfile.openclaw
More file actions
136 lines (109 loc) · 8.7 KB
/
Copy pathDockerfile.openclaw
File metadata and controls
136 lines (109 loc) · 8.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
# ── Stage 1: Builder ─────────────────────────────────────────────
FROM node:22-bookworm AS builder
RUN apt-get update && apt-get install -y --no-install-recommends git \
&& rm -rf /var/lib/apt/lists/*
RUN corepack enable
# Bun is required by OpenClaw build scripts
RUN curl -fsSL https://bun.sh/install | bash
ENV PATH="/root/.bun/bin:${PATH}"
WORKDIR /app
# Clone OpenClaw at a pinned release tag.
# Using git clone (vs upstream Dockerfile's staged COPY) ensures ALL files —
# including extensions/*/package.json — are present before pnpm install,
# so pnpm properly resolves and links extension dependencies
# (e.g. google-auth-library for googlechat).
ARG OPENCLAW_VERSION=v2026.4.15
RUN git clone --depth 1 --branch ${OPENCLAW_VERSION} https://github.com/openclaw/openclaw.git .
RUN pnpm install --frozen-lockfile
RUN NODE_OPTIONS="--max-old-space-size=6144" OPENCLAW_A2UI_SKIP_MISSING=1 pnpm build
ENV OPENCLAW_PREFER_PNPM=1
# Skip UI build — LifeOS serves its own dashboard, OpenClaw runs headless
# Clean up build-only artifacts. We intentionally skip `pnpm prune --prod`
# because extensions (e.g. googlechat) are loaded dynamically at runtime and
# their production deps (e.g. google-auth-library) would be dropped since no
# root-level production dependency references them through the dependency graph.
RUN rm -rf .git node_modules/.cache /root/.local/share/pnpm/store
# ── Stage 2: Runtime ─────────────────────────────────────────────
FROM node:22-bookworm-slim
# git: runtime use + Homebrew
# curl, file: Homebrew core
# procps: Homebrew health checks (ps)
# build-essential: compile Homebrew formulas that lack prebuilt bottles
RUN apt-get update && apt-get install -y --no-install-recommends \
git curl ca-certificates file procps build-essential python3 \
chromium \
&& rm -rf /var/lib/apt/lists/*
# Tell Puppeteer / Playwright to use the system Chromium instead of downloading their own
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true
ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium
ENV CHROME_BIN=/usr/bin/chromium
RUN corepack enable
WORKDIR /app
COPY --from=builder /app /app
COPY file-server.mjs /app/file-server.mjs
# node-pty (native PTY for interactive terminal) + ws (WebSocket server)
RUN npm install --prefix /opt/file-server-deps node-pty ws
ENV NODE_ENV=production
ENV OPENCLAW_PREFER_PNPM=1
# Persist V8 compile cache so subsequent openclaw commands start faster
ENV NODE_COMPILE_CACHE=/app/.compile-cache
# Pre-create Homebrew prefix as root so `node` user can install there
RUN mkdir -p /home/linuxbrew/.linuxbrew \
&& chown -R node:node /home/linuxbrew/.linuxbrew
# NOTE: Do not chown /usr/local/bin — doing so triggers an overlay FS copy-up
# of /usr/local/bin/node, which invalidates the V8 compile cache key between
# build-time warmup and runtime. Cache dir `v22.22.2-x64-<hash>-<uid>` — hash
# derived from Node binary identity, so touching it breaks cache reuse across
# build and runtime. All /usr/local/bin writes must happen as root, before
# the USER node switch below.
# Pre-install LifeOS CLI so pods have it on first boot (root — writes to /usr/local/lib)
RUN npm install -g lifeos-cli
# GitHub CLI — installed as standalone binary to /usr/local/bin because
# the Homebrew prefix is overlaid by a PVC mount at runtime in K8s.
RUN GH_VERSION=$(curl -fsSL https://api.github.com/repos/cli/cli/releases/latest | grep '"tag_name"' | sed 's/.*"v\(.*\)".*/\1/') \
&& curl -fsSL "https://github.com/cli/cli/releases/download/v${GH_VERSION}/gh_${GH_VERSION}_linux_amd64.tar.gz" \
| tar -xz --strip-components=2 -C /usr/local/bin "gh_${GH_VERSION}_linux_amd64/bin/gh"
# Provide an openclaw executable (root — writes to /usr/local/bin)
RUN printf '%s\n' '#!/usr/bin/env bash' 'exec node /app/openclaw.mjs "$@"' > /usr/local/bin/openclaw \
&& chmod +x /usr/local/bin/openclaw
# Expose the Pi coding-agent binary (root — writes to /usr/local/bin)
RUN printf '%s\n' '#!/usr/bin/env bash' 'exec /app/node_modules/.bin/pi "$@"' > /usr/local/bin/pi \
&& chmod +x /usr/local/bin/pi
# Stage LifeOS skills for seeding into user PVCs
COPY packages/cli/skills/ /app/lifeos-skills/
# Also place skills in the bundled skills dir so OpenClaw's skill loader picks them up natively
COPY packages/cli/skills/ /app/skills/
RUN chown -R node:node /app
USER node
# Homebrew (Linuxbrew) — needed so OpenClaw skill install buttons work.
RUN NONINTERACTIVE=1 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
ENV HOMEBREW_PREFIX="/home/linuxbrew/.linuxbrew"
ENV PATH="${HOMEBREW_PREFIX}/bin:${HOMEBREW_PREFIX}/sbin:${PATH}"
# Pre-warm the V8 compile cache by booting the gateway with a synthetic config
# that mirrors the runtime config shape as closely as possible, so every module
# the runtime imports is also imported here and its compiled bytecode lands in
# /app/.compile-cache. Discard-port (127.0.0.1:9) baseUrls keep us offline.
#
# Rules that govern what gets cached:
# 1. A module only enters the cache if it's IMPORTED during warmup.
# 2. OpenClaw loads provider adapters lazily, keyed on `api` (anthropic-messages,
# openai-responses, openai-completions). Include one provider per adapter.
# 3. `tools.media.audio`, `skills.load`, `gateway.trustedProxies`,
# `controlUi.allowedOrigins`, and per-provider `request.allowPrivateNetwork`
# each trigger their own module graph.
# 4. Channel init happens AFTER the HTTP server binds — sleep after port 200
# so "starting channels and sidecars…" completes before we kill.
RUN mkdir -p /tmp/warmup-home/.openclaw \
&& echo '{"gateway":{"auth":{"mode":"token","token":"warmup-token"},"controlUi":{"dangerouslyDisableDeviceAuth":true,"allowedOrigins":["http://localhost:4101","https://lifeos.zone"]},"trustedProxies":["10.0.0.0/8"]},"browser":{"enabled":true,"headless":true,"noSandbox":true,"defaultProfile":"openclaw"},"models":{"mode":"replace","providers":{"anthropic":{"baseUrl":"http://127.0.0.1:9","apiKey":"warmup","api":"anthropic-messages","headers":{"X-Warmup":"1"},"request":{"allowPrivateNetwork":true},"models":[{"id":"claude-opus-4-6","name":"Claude Opus","contextWindow":1000000,"maxTokens":32000,"input":["text","image"]}]},"openai":{"baseUrl":"http://127.0.0.1:9","apiKey":"warmup","api":"openai-responses","headers":{"X-Warmup":"1"},"request":{"allowPrivateNetwork":true},"models":[{"id":"gpt-5.4-2026-03-05","name":"GPT 5.4","contextWindow":1048576,"maxTokens":32000,"input":["text","image"]}]},"kimi":{"baseUrl":"http://127.0.0.1:9","apiKey":"warmup","api":"openai-completions","headers":{"X-Warmup":"1"},"request":{"allowPrivateNetwork":true},"models":[{"id":"moonshotai/kimi-k2-thinking-maas","name":"Kimi","contextWindow":262144,"maxTokens":65536}]},"gemini":{"baseUrl":"http://127.0.0.1:9","apiKey":"warmup","api":"openai-completions","headers":{"X-Warmup":"1"},"request":{"allowPrivateNetwork":true},"models":[{"id":"google/gemini-3.1-pro-preview","name":"Gemini","contextWindow":1000000,"maxTokens":65536}]},"minimax":{"baseUrl":"http://127.0.0.1:9","apiKey":"warmup","api":"openai-completions","headers":{"X-Warmup":"1"},"request":{"allowPrivateNetwork":true},"models":[{"id":"MiniMax-M2.1","name":"MiniMax","contextWindow":200000,"maxTokens":8192}]},"qwen-vertex":{"baseUrl":"http://127.0.0.1:9","apiKey":"warmup","api":"openai-completions","headers":{"X-Warmup":"1"},"request":{"allowPrivateNetwork":true},"models":[{"id":"qwen/qwen3-coder-480b-a35b-instruct-maas","name":"Qwen","contextWindow":262144,"maxTokens":65536}]}}},"agents":{"defaults":{"model":{"primary":"openai/gpt-5.4-2026-03-05"}}},"tools":{"media":{"audio":{"enabled":true,"echoTranscript":true,"models":[{"provider":"openai","model":"gpt-4o-mini-transcribe"}]}}},"skills":{"load":{"extraDirs":["/tmp/warmup-home/.openclaw/skills"]}},"channels":{"telegram":{"enabled":true,"dmPolicy":"open","allowFrom":["*"]}},"plugins":{"deny":["whatsapp","discord","googlechat","acpx","device-pair","phone-control","talk-voice","memory-core","active-memory"],"entries":{"telegram":{"enabled":true}}}}' \
> /tmp/warmup-home/.openclaw/openclaw.json
RUN HOME=/tmp/warmup-home node --disable-warning=ExperimentalWarning openclaw.mjs gateway --allow-unconfigured &\
pid=$!; \
for i in $(seq 1 180); do \
if curl -sf http://127.0.0.1:18789 >/dev/null 2>&1; then break; fi; \
sleep 1; \
done; \
curl -sf http://127.0.0.1:18789/ >/dev/null 2>&1 || true; \
sleep 45; \
kill $pid 2>/dev/null; wait $pid 2>/dev/null || true; \
rm -rf /tmp/warmup-home
CMD ["node", "--disable-warning=ExperimentalWarning", "openclaw.mjs", "gateway", "--allow-unconfigured"]