Skip to content

Docker 與 Fly 部署

源码版本v2026.6.11

職責

Dockerfile(Dockerfile CMD:343-344) 是 OpenClaw 容器化 (containerization) 部署的根入口:多階段建置出一個最小 runtime 鏡像,預設啟動 node openclaw.mjs gatewayfly.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) 乾淨退出,剩下的交給編排器。

設計動機

為什麼容器化部署單獨抽一層?

  1. 鏡像要最小: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)。
  2. 安全預設: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)。
  3. 健康檢查內建:HEALTHCHECK(HEALTHCHECK:341-342)直接呼叫 /healthz 端點,不依賴外部探測指令碼——編排器(Docker Compose/Fly/Render)按這個判斷容器是否活著。/healthz 是 liveness、/readyz 是 readiness,別名 /health/ready 相容習慣。
  4. 跨編排器:fly.tomlrender.yamldocker-compose.yml 三套設定覆蓋三種部署目標,但底層都是同一個鏡像——差別只在連接埠/掛載/環境變數宣告方式。

關鍵檔案

  • Dockerfile build args:10-25OPENCLAW_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 enablepnpm 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-154pnpm prune --prod --offline,刪 dev dependencies、d.ts、.map、node_modules/openclaw(自參考)。
  • Stage 3: runtime:156-189 — base bookworm-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-305ln -sf /app/openclaw.mjs /usr/local/bin/openclaw,避免非 root npm 全域寫入。
  • 預建立目錄權限:312-320install -d -m 0700 -o node -g node,然後 stat -c 驗證,防止 root 擁有的目錄污染 named volume 首次掛載。
  • ENV + USER + HEALTHCHECK + CMD:322-344NODE_ENV=productionUSER nodeHEALTHCHECK 3min 間隔 + tini 入口 + CMD ["node","openclaw.mjs","gateway"]
  • fly.tomlapp = "openclaw"primary_region = "iad"internal_port = 3000auto_stop_machines = false(保持常駐)、min_machines_running = 1shared-cpu-2x + 2GB、openclaw_data volume 掛到 /data
  • render.yamlruntime: dockerplan: starterhealthCheckPath: /healthOPENCLAW_GATEWAY_TOKEN: generateValue: true(自動產生)、1GB disk 掛 /data
  • docker-compose.ymlopenclaw-gateway + openclaw-cli 兩服務,CLI 用 network_mode: service:openclaw-gateway 共享網路命名空間,掛載 ~/.openclaw 和 secrets 目錄。

資料流

容器啟動的最終幾步(Dockerfile 尾部:322-344)很關鍵:

dockerfile
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 還固定了容器側的環境變數路徑:

yaml
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:

toml
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 add seed tarball,再 --config.offline=true prune,避免 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-colonspub 行,要求正好 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.yamlOPENCLAW_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