Déploiement Docker et Fly
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?
- Image minimale: l'image complète
node:24-bookwormpèse 1GB+, alors que le runtime n'a besoin que denode:24-bookworm-slim+ quelques paquets systèmeca-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). - 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=0o077en mode daemon protège les permissions de fichiers; en conteneur on obtient le même effet en pré-créant des répertoires0700(install -d permissions:312-320). - 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./healthzpour liveness,/readyzpour readiness, alias/healthet/readypour la compat d'habitudes. - Cross-orchestrateur:
fly.toml,render.yaml,docker-compose.ymlcouvrent 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-25—OPENCLAW_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-154—pnpm prune --prod --offline, supprime dev dependencies, d.ts, .map,node_modules/openclaw(self-reference).Stage 3: runtime:156-189— basebookworm-slim, apt-get installca-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-305—ln -sf /app/openclaw.mjs /usr/local/bin/openclaw, évite l'écriture npm global non root.pré-création permissions répertoire:312-320—install -d -m 0700 -o node -g node, puisstat -cvérifie, pour empêcher un répertoire appartenant à root de polluer le premier montage named volume.ENV + USER + HEALTHCHECK + CMD:322-344—NODE_ENV=production,USER node,HEALTHCHECK3min interval + entréetini+CMD ["node","openclaw.mjs","gateway"].fly.toml—app = "openclaw",primary_region = "iad",internal_port = 3000,auto_stop_machines = false(rester résident),min_machines_running = 1,shared-cpu-2x+ 2GB, volumeopenclaw_datamonté sur/data.render.yaml—runtime: docker,plan: starter,healthCheckPath: /health,OPENCLAW_GATEWAY_TOKEN: generateValue: true(auto-généré), 1GB disk monté sur/data.docker-compose.yml— Deux servicesopenclaw-gateway+openclaw-cli, le CLI utilisenetwork_mode: service:openclaw-gatewaypour partager le network namespace, monte~/.openclawet le répertoire secrets.
Flux de données
Les dernières étapes du démarrage conteneur (Dockerfile tail:322-344) sont clés:
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:
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/workspaceCet 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:
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_IMAGEtous 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.nodesoit 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 siOPENCLAW_EXTENSIONSinclutmatrix. - A2UI cross-arch stub (
a2ui stub:113-118):pnpm canvas:a2ui:bundlepeut é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 viapnpm store add, puis--config.offline=trueprune, pour éviter que le prune ne doive aller sur le réseau. Après prune,node scripts/check-package-dist-imports.mjsvalide 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): uninstall -dsans-o nodecrée/home/node/.configen root:root, puis le sous-répertoireopenclawhérite root, le conteneur tournant ennodene peut pas écrire dedans (#85968). Le code fait d'abordinstall -d -m 0755 -o node -g node /home/node/.configpuis crée le sous-répertoire, etstat -cvérifie que les permissions sont exactement celles attendues. - GPG single key forcé pour Docker CLI (
docker gpg single key:283-289): quandOPENCLAW_INSTALL_DOCKER_CLI=1, après download de la clé apt signing Docker, on compte les lignespubviagpg --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.envhôte ne doit pas entrer dans le conteneur, donc le compose override forcéOPENCLAW_STATE_DIR/OPENCLAW_CONFIG_PATH/OPENCLAW_CONFIG_DIR/OPENCLAW_WORKSPACE_DIRvers 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 = 1est 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: truelaisse Render générer et injecter — pour éviter que l'utilisateur ne code en dur le token dans le yaml.healthCheckPath: /healthutilise 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 fairesetuidpour é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.