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