Skip to content

Déploiement Docker et Fly

源码版本v2026.6.11

Responsabilités

Le Dockerfile (Dockerfile CMD:343-344) est l'entrée racine du déploiement conteneurisé (containerization) d'OpenClaw: un build multi-stage produit une image runtime minimale qui lance par défaut node openclaw.mjs gateway. fly.toml (fly.toml) est le descripteur de déploiement Fly.io, qui monte l'image Docker sur la VM + volume persistant de Fly. render.yaml (render.yaml) est la config équivalente pour Render.com. docker-compose.yml (docker-compose.yml) orchestre deux services gateway + CLI, utilisable tel quel pour le dev local ou self-hosted.

Cette forme de déploiement est complémentaire de Daemon: service système: à l'intérieur du conteneur il n'y a pas launchd/systemd/schtasks; l'orchestrateur de conteneurs (Docker Engine / Fly machine / Render) est le « gestionnaire de services système », qui gère le démarrage auto et le restart sur crash. OpenClaw lui-même n'a qu'à exposer un endpoint de santé (health check) + accepter le signal standard (SIGTERM) pour une sortie propre, le reste est laissé à l'orchestrateur.

Motivation de conception

Pourquoi le déploiement conteneurisé mérite-t-il une couche séparée?

  1. Image minimale: l'image complète node:24-bookworm pèse 1GB+, alors que le runtime n'a besoin que de node:24-bookworm-slim + quelques paquets système ca-certificates/curl/git/tini. Le build multi-stage sépare la couche de build (Bun/pnpm install + pnpm build:docker + pnpm ui:build) et la couche runtime; l'image finale n'embarque ni Bun, ni source, ni dev dependencies, ni .d.ts/.map (prune d.ts and maps:149).
  2. Sécurité par défaut: USER node (USER node:327), tourne non root; cap_drop: [NET_RAW, NET_ADMIN] + security_opt: [no-new-privileges:true] (cap_drop), le conteneur ne peut pas construire de raw socket ni élever ses privilèges. Umask=0o077 en mode daemon protège les permissions de fichiers; en conteneur on obtient le même effet en pré-créant des répertoires 0700 (install -d permissions:312-320).
  3. Health check builtin: HEALTHCHECK (HEALTHCHECK:341-342) appelle directement /healthz, sans dépendre d'un script de probe externe — l'orchestrateur (Docker Compose/Fly/Render) l'utilise pour juger si le conteneur est vivant. /healthz pour liveness, /readyz pour readiness, alias /health et /ready pour la compat d'habitudes.
  4. Cross-orchestrateur: fly.toml, render.yaml, docker-compose.yml couvrent trois cibles de déploiement, mais toutes basées sur la même image — seules diffèrent les déclarations de port/montage/variables d'environnement.

Fichiers clés

  • 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 — Copie seulement la liste des package.json, pour éviter que la couche main build ne soit invalidée par des changements de source non liés.
  • Stage 2: build:48-99 — COPY Bun binary, corepack enable, pnpm install --frozen-lockfile, retry native addon matrix-sdk-crypto, pnpm build:docker + pnpm ui:build.
  • matrix-sdk-crypto retry:82-97 — 5 retries pour télécharger le native addon, car le CDN de matrix-sdk-crypto-nodejs est parfois instable.
  • runtime-assets:135-154pnpm prune --prod --offline, supprime dev dependencies, d.ts, .map, node_modules/openclaw (self-reference).
  • Stage 3: runtime:156-189 — base bookworm-slim, apt-get install ca-certificates curl git hostname lsof openssl procps python3 tini.
  • ca-certificates install:184-189 — Sans ca-certificates, l'HTTPS outbound échouerait au TLS handshake (error setting certificate file).
  • OPENCLAW_INSTALL_BROWSER:252-262 — Optionnel Playwright Chromium + Xvfb, pour éviter un install de 60-90s à chaque démarrage.
  • OPENCLAW_INSTALL_DOCKER_CLI:268-301 — Optionnel Docker CLI, nécessaire pour la gestion des conteneurs sandbox; double vérification fingerprint GPG, exige exactement un pub key.
  • CLI symlink:304-305ln -sf /app/openclaw.mjs /usr/local/bin/openclaw, évite l'écriture npm global non root.
  • pré-création permissions répertoire:312-320install -d -m 0700 -o node -g node, puis stat -c vérifie, pour empêcher un répertoire appartenant à root de polluer le premier montage named volume.
  • ENV + USER + HEALTHCHECK + CMD:322-344NODE_ENV=production, USER node, HEALTHCHECK 3min interval + entrée tini + CMD ["node","openclaw.mjs","gateway"].
  • fly.tomlapp = "openclaw", primary_region = "iad", internal_port = 3000, auto_stop_machines = false (rester résident), min_machines_running = 1, shared-cpu-2x + 2GB, volume openclaw_data monté sur /data.
  • render.yamlruntime: docker, plan: starter, healthCheckPath: /health, OPENCLAW_GATEWAY_TOKEN: generateValue: true (auto-généré), 1GB disk monté sur /data.
  • docker-compose.yml — Deux services openclaw-gateway + openclaw-cli, le CLI utilise network_mode: service:openclaw-gateway pour partager le network namespace, monte ~/.openclaw et le répertoire secrets.

Flux de données

Les dernières étapes du démarrage conteneur (Dockerfile tail:322-344) sont clés:

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", "--"] est clé — tini est un init léger, qui reape les processus zombies (zombie process) et transfère correctement les signaux au process Node. À l'intérieur d'un conteneur, PID 1 ne traite pas SIGCHLD par défaut; sans tini, les sous-process ACP/subagent se transformeraient en zombies à leur sortie; tini transfère aussi proprement le SIGTERM envoyé par docker stop à Node, pour que la gateway fasse une sortie propre plutôt que d'être SIGKILL'd au bout de 10s.

HEALTHCHECK utilise le fetch global de Node (Node 18+), pas besoin de curl, appelle directement /healthz. --start-period=15s laisse un buffer pour le bootstrap gateway (chargement plugin, connexion channel), 3 minutes de fréquence de probe est assez opportune sans être bruyante. --retries=3 signifie qu'il faut 3 échecs consécutifs pour marquer unhealthy, pour éviter les faux positifs sur un jitter ponctuel.

Le health check de docker-compose.yml est plus agressif: 30s interval, 5 retries, 20s start_period (compose healthcheck). Le compose override le --bind lan par défaut, car en réseau bridge un bind loopback n'est pas accessible depuis l'hôte — c'est le piège explicitement warné dans les commentaires du Dockerfile. Le compose fixe aussi les chemins de variables d'environnement côté conteneur:

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

Cet override fixe #77436 — l'utilisateur écrivait OPENCLAW_STATE_DIR=/Users/alex/.openclaw dans .env, le compose l'injectait dans le conteneur, Node tentait mkdir '/Users' et EACCES. Le compose override forcé côté conteneur ces chemins vers /home/node/.openclaw; le chemin hôte ne sert que de source pour le bind-mount.

fly.toml (fly.toml) déploie la même image sur 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   # rester résident
auto_start_machines = true
min_machines_running = 1
processes = ["app"]

[[vm]]
size = "shared-cpu-2x"
memory = "2048mb"

[mounts]
source = "openclaw_data"
destination = "/data"

Fly utilise internal_port = 3000 pour le health (correspondant au OPENCLAW_GATEWAY_PORT du compose), force_https = true laisse la couche edge de Fly terminer le TLS, auto_stop_machines = false + min_machines_running = 1 garantit que les connexions longues WebSocket ne soient pas tuées par le scale-to-zero de Fly — c'est critique pour un déploiement agent system: la gateway doit rester résidente, sinon tous les clients perdent leur session. NODE_OPTIONS = "--max-old-space-size=1536" sur une VM 2GB laisse ~500MB pour Bun/native addon/Playwright, évite OOM kill.

Limites et modes d'échec

  • Pin SHA base image: OPENCLAW_NODE_BOOKWORM_IMAGE/OPENCLAW_NODE_BOOKWORM_SLIM_IMAGE/OPENCLAW_BUN_IMAGE tous pin au digest SHA256 (digest pin:12-17). Dependabot met à jour ces digests blessed — le build release consomme des snapshots audités plutôt que de tirer le dernier à chaque build, pour garantir reproducible build.
  • Retry download matrix-sdk-crypto (matrix-sdk-crypto retry:82-97): le CDN du native addon matrix-sdk-crypto-nodejs est instable, peut exit 0 sans que le fichier .node soit téléchargé. Le code fait 5 retries + sleep $((attempt * 2)) backoff exponentiel, exit 1 au final si échec. Ce check ne s'active que si OPENCLAW_EXTENSIONS inclut matrix.
  • A2UI cross-arch stub (a2ui stub:113-118): pnpm canvas:a2ui:bundle peut échouer sur un build QEMU cross-arch (Apple Silicon build amd64), le CI build nativement par arch donc non concerné. Fallback sur stub (non fatal), pour que le build croisé local ne crashe pas sur le bundle UI.
  • OPENCLAW_PREFER_PNPM=1 (OPENCLAW_PREFER_PNPM:124): le UI build utilise pnpm plutôt que Bun, Bun a des problèmes de compat sur ARM/Synology.
  • prune --prod --offline (runtime-assets prune:140-154): seed des tarballs via pnpm store add, puis --config.offline=true prune, pour éviter que le prune ne doive aller sur le réseau. Après prune, node scripts/check-package-dist-imports.mjs valide les import boundaries, pour empêcher qu'une dépendance dev-only ne passe dans l'image prod.
  • Piège permissions non-root /home/node/.config (config dir ownership:309-320): un install -d sans -o node crée /home/node/.config en root:root, puis le sous-répertoire openclaw hérite root, le conteneur tournant en node ne peut pas écrire dedans (#85968). Le code fait d'abord install -d -m 0755 -o node -g node /home/node/.config puis crée le sous-répertoire, et stat -c vérifie que les permissions sont exactement celles attendues.
  • GPG single key forcé pour Docker CLI (docker gpg single key:283-289): quand OPENCLAW_INSTALL_DOCKER_CLI=1, après download de la clé apt signing Docker, on compte les lignes pub via gpg --show-keys --with-colons, exige exactement 1 — un fichier multi-keys est refusé à l'import, pour empêcher « apt truste la première key du fichier, mais la key effectivement trustée est une autre ».
  • Fuite de chemin bind-mount (compose env override) : un chemin macOS dans le .env hôte ne doit pas entrer dans le conteneur, donc le compose override forcé OPENCLAW_STATE_DIR/OPENCLAW_CONFIG_PATH/OPENCLAW_CONFIG_DIR/OPENCLAW_WORKSPACE_DIR vers des chemins fixes sous /home/node. La source du bind-mount utilise ${HOME}/.openclaw, l'hôte peut encore spécifier.
  • Sémantique Fly résident: auto_stop_machines = false + min_machines_running = 1 est une contrainte dure pour le déploiement d'une gateway agent — par défaut Fly scale-to-zero pour économiser, mais les connexions longues WebSocket coupées, toutes les sessions IDE/client perdues. Le déploiement Fly doit explicitement désactiver l'auto-stop. shared-cpu-2x + 2GB est la config minimum raisonnable, en dessous Node OOM.
  • Render auto-génère token: dans render.yaml, OPENCLAW_GATEWAY_TOKEN: generateValue: true laisse Render générer et injecter — pour éviter que l'utilisateur ne code en dur le token dans le yaml. healthCheckPath: /health utilise l'alias plutôt que /healthz, Render est plus habitué à ce chemin.
  • cap_drop: [NET_RAW, NET_ADMIN] + no-new-privileges: même si le conteneur est compromis, il ne peut pas construire de raw socket (anti port scan/ARP spoofing), ni faire setuid pour élever ses privilèges. C'est la baseline de défense en profondeur (defense-in-depth) d'un déploiement conteneurisé, que le mode daemon ne peut pas atteindre.

Résumé

Le déploiement conteneurisé remplace launchd/systemd/schtasks par Docker Engine/Fly machine/Render, mais le partitionnement des responsabilités OpenClaw reste: le process gateway fait toujours node openclaw.mjs gateway (voir cœur de la Gateway), juste que l'entrypoint enveloppe tini pour gérer signaux + zombies, le health check s'appuie sur les endpoints builtin /healthz//readyz. Trois cibles de déploiement (Docker Compose local self-hosted / Fly.io cloud / Render.com cloud) partagent le même Dockerfile, ne diffèrent que sur les montages de volume spécifiques à l'orchestrateur, les specs ressources, le mode d'injection du token. Les déploiements bare-metal ou VM sans Docker peuvent aller par Daemon: service système pour installer directement comme service système; le fichier de config (openclaw.json) est identique dans les deux formes de déploiement, voir openclaw.json.