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 の 2 サービスを編成し,ローカル開発でもセルフホストでも直接使えます。
このデプロイ形態は 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の 3 セット設定で 3 種のデプロイターゲットをカバーしますが,底はすべて同じイメージ——違いはポート/マウント/環境変数宣言方式だけです。
主要ファイル
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— native addon ダウンロードを 5 回再試行,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 二次検証,ちょうど 1 個の 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-cli2 サービス,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 で,ゾンビプロセス (zombie process) を reap し,信号を 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(プラグインロード、channel 接続)にバッファを与え,3 分 1 回のプローブ頻度は十分タイムリーだが喧騒ではありません。--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はすべて SHA256 digest に pin(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 クロスアーキ stub(
a2ui stub:113-118):pnpm canvas:a2ui:bundleは QEMU クロスアーキビルド(Apple Silicon で amd64 構築)時に失敗することがあり,CI はネイティブ毎アーキビルドなので影響ありません。ここでは stub に fallback(非致命的)し,ローカルクロスビルドが UI bundle で止まるのを回避します。 OPENCLAW_PREFER_PNPM=1(OPENCLAW_PREFER_PNPM:124):UI build は Bun ではなく pnpm を使い,Bun は ARM/Synology アーキテクチャに互換問題があるためです。prune --prod --offline(runtime-assets prune:140-154):先にpnpm store addで tarball を seed し,それから--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-colonsでpub行を数え,ちょうど 1 個を要求します——多 key ファイルはインポートを拒否し,「apt が多 key ファイルの最初の key を信頼したつもりが実は別の key を信頼していた」を防ぎます。 - bind-mount パス漏洩(
compose env override):hostの.envにある macOS パスはコンテナに入れられないので,compose はOPENCLAW_STATE_DIR/OPENCLAW_CONFIG_PATH/OPENCLAW_CONFIG_DIR/OPENCLAW_WORKSPACE_DIRの 4 変数を/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 エンドポイントに頼ります。3 種のデプロイターゲット(Docker Compose ローカルセルフホスト / Fly.io クラウド / Render.com クラウド)は同じ Dockerfile を共有し,差はオーケストレータ固有のボリュームマウント、リソース仕様、token 注入方式だけです。物理機や VM デプロイは Docker を必要とせず,Daemon:システムサービス で直接システムサービスとしてインストール可能;設定ファイル自体(openclaw.json)は両デプロイ形態で同じ,openclaw.json を参照。