Docker 與 Fly 部署
職責
Dockerfile(Dockerfile CMD:343-344) 是 OpenClaw 容器化 (containerization) 部署的根入口:多階段建置出一個最小 runtime 鏡像,預設啟動 node openclaw.mjs gateway。fly.toml(fly.toml) 是 Fly.io 的部署描述,把 Docker 鏡像掛到 Fly 的 vm + 持久卷上。render.yaml(render.yaml) 是 Render.com 的等價設定。docker-compose.yml(docker-compose.yml) 編排 gateway + CLI 兩個服務,本地開發或自托管都能直接用。
這套部署形態和 Daemon:系統服務 互補:容器裡沒有 launchd/systemd/schtasks,容器編排器(Docker Engine / Fly machine / Render)就是「系統服務管理器」,負責開機自啟和崩潰重啟。OpenClaw 自己只需要暴露健康檢查 (health check) 端點 + 接受標準訊號 (SIGTERM) 乾淨退出,剩下的交給編排器。
設計動機
為什麼容器化部署單獨抽一層?
- 鏡像要最小:
node:24-bookworm完整鏡像有 1GB+,而執行時只需要node:24-bookworm-slim+ca-certificates/curl/git/tini等少量系統套件。多階段建置把建置層(Bun/pnpm install +pnpm build:docker+pnpm ui:build)和執行層分開,最終鏡像不帶 Bun、不帶原始碼、不帶 dev dependencies、不帶 .d.ts/.map(prune d.ts and maps:149)。 - 安全預設:
USER node(USER node:327),非 root 執行;cap_drop: [NET_RAW, NET_ADMIN]+security_opt: [no-new-privileges:true](cap_drop),容器內不能構造 raw socket、不能提升權限。Umask=0o077在 daemon 模式下保護檔案權限,容器裡透過預建立0700目錄達到同樣的效果(install -d permissions:312-320)。 - 健康檢查內建:
HEALTHCHECK(HEALTHCHECK:341-342)直接呼叫/healthz端點,不依賴外部探測指令碼——編排器(Docker Compose/Fly/Render)按這個判斷容器是否活著。/healthz是 liveness、/readyz是 readiness,別名/health和/ready相容習慣。 - 跨編排器:
fly.toml、render.yaml、docker-compose.yml三套設定覆蓋三種部署目標,但底層都是同一個鏡像——差別只在連接埠/掛載/環境變數宣告方式。
關鍵檔案
Dockerfile build args:10-25—OPENCLAW_EXTENSIONS=""、OPENCLAW_BUNDLED_PLUGIN_DIR=extensions、pinned base image digest(node:24-bookworm / bookworm-slim / Bun)。Stage 1: workspace-deps:26-45— 只複製 package.json 列表,避免 main build layer 被無關原始碼改動 invalidate。Stage 2: build:48-99— COPY Bun binary、corepack enable、pnpm install --frozen-lockfile、matrix-sdk-crypto native addon 重試、pnpm build:docker+pnpm ui:build。matrix-sdk-crypto 重試:82-97— 5 次重試下載 native addon,因為 matrix-sdk-crypto-nodejs 的 CDN 有時不穩定。runtime-assets:135-154—pnpm prune --prod --offline,刪 dev dependencies、d.ts、.map、node_modules/openclaw(自參考)。Stage 3: runtime:156-189— basebookworm-slim,裝ca-certificates curl git hostname lsof openssl procps python3 tini。ca-certificates install:184-189— 不裝會讓 HTTPS outbound 在 TLS handshake 失敗(error setting certificate file)。OPENCLAW_INSTALL_BROWSER:252-262— 可選 Playwright Chromium + Xvfb,避免每次啟動 60-90s install。OPENCLAW_INSTALL_DOCKER_CLI:268-301— 可選 Docker CLI,sandbox 容器管理需要;GPG fingerprint 二次校驗,要求正好一個 pub key。CLI symlink:304-305—ln -sf /app/openclaw.mjs /usr/local/bin/openclaw,避免非 root npm 全域寫入。預建立目錄權限:312-320—install -d -m 0700 -o node -g node,然後stat -c驗證,防止 root 擁有的目錄污染 named volume 首次掛載。ENV + USER + HEALTHCHECK + CMD:322-344—NODE_ENV=production、USER node、HEALTHCHECK3min 間隔 +tini入口 +CMD ["node","openclaw.mjs","gateway"]。fly.toml—app = "openclaw"、primary_region = "iad"、internal_port = 3000、auto_stop_machines = false(保持常駐)、min_machines_running = 1、shared-cpu-2x+ 2GB、openclaw_datavolume 掛到/data。render.yaml—runtime: docker、plan: starter、healthCheckPath: /health、OPENCLAW_GATEWAY_TOKEN: generateValue: true(自動產生)、1GB disk 掛/data。docker-compose.yml—openclaw-gateway+openclaw-cli兩服務,CLI 用network_mode: service:openclaw-gateway共享網路命名空間,掛載~/.openclaw和 secrets 目錄。
資料流
容器啟動的最終幾步(Dockerfile 尾部:322-344)很關鍵:
ENV NODE_ENV=production
# Security hardening: Run as non-root user
# The node:24-bookworm image includes a 'node' user (uid 1000)
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
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"]ENTRYPOINT ["tini", "-s", "--"] 是關鍵——tini 是個輕量 init,負責 reap 殭屍進程 (zombie process) 並把訊號正確轉發給 Node 進程。容器裡 PID 1 預設不處理 SIGCHLD,沒有 tini 的話 ACP/subagent 子進程退出後會變成僵屍;tini 還會把 docker stop 發的 SIGTERM 乾淨地轉給 Node,讓 gateway 走乾淨退出而不是 10s 後被 SIGKILL。
HEALTHCHECK 用 Node 自帶的 fetch(Node 18+ 全域可用),不需要 curl,直接呼叫 /healthz。--start-period=15s 給 gateway bootstrap 留緩衝(裝載 plugin、連 channel),3 分鐘一次的探測頻率足夠及時但不喧鬧。--retries=3 意味著連續 3 次失敗才標 unhealthy,避免偶發抖動誤判。
docker-compose.yml 的健康檢查更激進:30s 間隔、5 次 retries、20s start_period(compose healthcheck)。compose 的指令列覆蓋了預設 --bind lan,因為 bridge 網路下 loopback bind 無法從 host 存取——這是 Dockerfile 註解裡明確警告的坑。compose 還固定了容器側的環境變數路徑:
environment:
HOME: /home/node
OPENCLAW_HOME: /home/node
# Pin container-side state, workspace, and config paths so host values written to
# `.env` (used by Compose for the bind-mount source below) cannot leak
# into runtime code that resolves these env vars inside the container.
# Without this override, a macOS host path like /Users/<you>/.openclaw/...
# imported from .env caused first-reply `mkdir '/Users'` EACCES failures
# in Linux Docker (#77436).
OPENCLAW_STATE_DIR: /home/node/.openclaw
OPENCLAW_CONFIG_PATH: /home/node/.openclaw/openclaw.json
OPENCLAW_CONFIG_DIR: /home/node/.openclaw
OPENCLAW_WORKSPACE_DIR: /home/node/.openclaw/workspace這個覆蓋修了 #77436——使用者在 .env 裡寫 OPENCLAW_STATE_DIR=/Users/alex/.openclaw,compose 把它注入容器,容器裡的 Node 試圖 mkdir '/Users' 就 EACCES。所以 compose 強制覆蓋容器側這些路徑為 /home/node/.openclaw,host 路徑只作為 bind-mount source。
fly.toml(fly.toml)把同一個鏡像部署到 Fly.io:
app = "openclaw"
primary_region = "iad"
[build]
dockerfile = "Dockerfile"
[env]
NODE_ENV = "production"
OPENCLAW_PREFER_PNPM = "1"
OPENCLAW_STATE_DIR = "/data"
NODE_OPTIONS = "--max-old-space-size=1536"
[processes]
app = "node dist/index.js gateway --allow-unconfigured --port 3000 --bind lan"
[http_service]
internal_port = 3000
force_https = true
auto_stop_machines = false # 保持常駐
auto_start_machines = true
min_machines_running = 1
processes = ["app"]
[[vm]]
size = "shared-cpu-2x"
memory = "2048mb"
[mounts]
source = "openclaw_data"
destination = "/data"Fly 用 internal_port = 3000 探測健康(對應 compose 的 OPENCLAW_GATEWAY_PORT),force_https = true 讓 Fly 邊緣層終止 TLS,auto_stop_machines = false + min_machines_running = 1 保證 WebSocket 長連線不會被 Fly 的 scale-to-zero 殺掉——這是 agent 系統部署的關鍵:gateway 必須常駐,斷了所有用戶端會話都會掉。NODE_OPTIONS = "--max-old-space-size=1536" 配合 2GB vm 記憶體,留 ~500MB 給 Bun/native addon/Playwright,避免 OOM kill。
邊界與失敗
- base image SHA pin:
OPENCLAW_NODE_BOOKWORM_IMAGE/OPENCLAW_NODE_BOOKWORM_SLIM_IMAGE/OPENCLAW_BUN_IMAGE全部 pin 到 SHA256 digest(digest pin:12-17)。Dependabot 會更新這些 blessed digest——release 建置消費審過的快照而不是每次建置都拉最新,保證 reproducible build。 - matrix-sdk-crypto 下載重試(
matrix-sdk-crypto retry:82-97):matrix-sdk-crypto-nodejs 的 native addon CDN 不穩定,可能 exit 0 但.node檔案沒下載到。程式碼 5 次重試 +sleep $((attempt * 2))指數退避,最後仍失敗則 exit 1。只在OPENCLAW_EXTENSIONS包含matrix時才做這個檢查。 - A2UI cross-arch stub(
a2ui stub:113-118):pnpm canvas:a2ui:bundle在 QEMU 跨架構建置(Apple Silicon 上建 amd64)時可能失敗,CI 是原生每架構建置所以不受影響。這裡 fallback 成 stub(非致命),讓本地交叉建置不至於因為 UI bundle 掛掉。 OPENCLAW_PREFER_PNPM=1(OPENCLAW_PREFER_PNPM:124):UI build 用 pnpm 而不是 Bun,Bun 在 ARM/Synology 架構上有相容問題。prune --prod --offline(runtime-assets prune:140-154):先pnpm store addseed tarball,再--config.offline=trueprune,避免 prune 時還要聯網。prune 後還node scripts/check-package-dist-imports.mjs校驗 import 邊界,防止 prod 鏡像裡出現 dev-only 依賴。- 非 root 但
/home/node/.config權限坑(config dir ownership:309-320):install -d不帶-o node會把/home/node/.config建立成 root:root,然後openclaw子目錄繼承 root 擁有,容器以node使用者跑時寫不進去(#85968)。程式碼先install -d -m 0755 -o node -g node /home/node/.config再建立子目錄,並用stat -c驗證權限完全符合預期。 - Docker CLI GPG 單 key 強制(
docker gpg single key:283-289):install OPENCLAW_INSTALL_DOCKER_CLI=1時,下載 Docker apt signing key 後gpg --show-keys --with-colons數pub行,要求正好 1 個——多 key 的檔案拒絕匯入,防止「apt 信任了多 key 檔案中第一個 key,但實際信任的是另一個」。 - bind-mount 路徑洩漏(
compose env override):host.env裡的 macOS 路徑不能進容器,所以 compose 強制覆蓋OPENCLAW_STATE_DIR/OPENCLAW_CONFIG_PATH/OPENCLAW_CONFIG_DIR/OPENCLAW_WORKSPACE_DIR四個變數為/home/node下的固定路徑。bind-mount source 用${HOME}/.openclaw,host 側仍可指定。 - Fly 常駐語意:
auto_stop_machines = false+min_machines_running = 1是 agent gateway 部署的硬約束——Fly 預設會 scale-to-zero 省錢,但 WebSocket 長連線斷了所有 IDE/用戶端會話都會丟。Fly 部署必須顯式關掉自動停機。shared-cpu-2x+ 2GB 是最低合理配置,低於這個記憶體 Node 會 OOM。 - Render 自動產生 token:
render.yaml裡OPENCLAW_GATEWAY_TOKEN: generateValue: true讓 Render 平台自動產生並注入——避免使用者在 yaml 裡硬編碼 token。healthCheckPath: /health用別名而非/healthz,Render 文件更習慣這個路徑。 cap_drop: [NET_RAW, NET_ADMIN]+no-new-privileges:容器即使被攻破也不能構造 raw socket(防連接埠掃描/ARP 欺騙)、不能setuid提權。這是容器化部署的縱深防禦 (defense-in-depth) 基線,daemon 模式做不到這個層級的隔離。
小結
容器化部署把 launchd/systemd/schtasks 換成了 Docker Engine/Fly machine/Render,但 OpenClaw 的職責劃分不變:gateway 進程依然跑 node openclaw.mjs gateway(見 Gateway 核心),只是入口包了 tini 處理訊號 + 殭屍進程,健康檢查靠內建的 /healthz//readyz 端點。三種部署目標(Docker Compose 本地自托管 / Fly.io 雲端 / Render.com 雲端)共享同一個 Dockerfile,差異只在編排器特定的卷掛載、資源規格、token 注入方式。實體機或 VM 部署不需要 Docker,可以走 Daemon:系統服務 直接裝成系統服務;設定檔本身(openclaw.json)在兩種部署形態下都一樣,見 openclaw.json。