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。