Skip to content

Docker- und Fly-Deployment

源码版本v2026.6.11

Verantwortung

Dockerfile (Dockerfile CMD:343-344) ist der Wurzel-Eintritt des containerisierten (containerization) Deployments von OpenClaw: Mehrstufiger Build erzeugt ein minimales Runtime-Image, das per Default node openclaw.mjs gateway startet. fly.toml (fly.toml) ist die Deploy-Beschreibung für Fly.io, die das Docker-Image auf Flys VM + persistenten Volume hängt. render.yaml (render.yaml) ist die äquivalente Konfiguration für Render.com. docker-compose.yml (docker-compose.yml) orchestriert zwei Services (gateway + CLI) und ist direkt für lokale Entwicklung oder Self-Hosting nutzbar.

Diese Deploy-Form ist komplementär zu Daemon: Systemdienst: Im Container gibt es kein launchd/systemd/schtasks; der Container-Orchestrator (Docker Engine / Fly machine / Render) ist der „Systemdienstmanager" und übernimmt Boot-Autostart und Crash-Neustart. OpenClaw selbst muss nur einen Health-Check-Endpunkt exponieren + Standard-Signal (SIGTERM) für sauberes Beenden akzeptieren; den Rest übernimmt der Orchestrator.

Designmotivation

Warum das containerisierte Deployment in einer eigenen Schicht?

  1. Image minimal: Das vollständige node:24-bookworm-Image ist über 1 GB groß, aber zur Laufzeit reichen node:24-bookworm-slim + wenige Systempakete wie ca-certificates/curl/git/tini. Der Mehrstufige Build trennt Build-Schicht (Bun/pnpm install + pnpm build:docker + pnpm ui:build) und Laufzeit-Schicht; das finale Image enthält kein Bun, keinen Source, keine Dev-Dependencies, keine .d.ts/.map (prune d.ts and maps:149).
  2. Sichere Defaults: USER node (USER node:327) läuft nicht als root; cap_drop: [NET_RAW, NET_ADMIN] + security_opt: [no-new-privileges:true] (cap_drop) verhindert, dass im Container Raw-Sockets konstruiert oder Privilegien erhöht werden. Umask=0o077 schützt im Daemon-Modus die Dateirechte; im Container wird dasselbe durch vorab angelegte 0700-Verzeichnisse erreicht (install -d permissions:312-320).
  3. Health-Check eingebaut: HEALTHCHECK (HEALTHCHECK:341-342) ruft direkt den /healthz-Endpunkt, ohne externes Probe-Skript — der Orchestrator (Docker Compose/Fly/Render) beurteilt danach, ob der Container lebt. /healthz ist Liveness, /readyz Readiness; die Aliase /health und /ready decken Gewohnheiten ab.
  4. Orchestrator-übergreifend: fly.toml, render.yaml, docker-compose.yml — drei Konfigurationen für drei Deploy-Ziele, aber unterlegt dasselbe Image; Unterschiede liegen nur in Port/Mount/Umgebungsvariablen-Deklaration.

Schlüsseldateien

  • Dockerfile build args:10-25OPENCLAW_EXTENSIONS="", OPENCLAW_BUNDLED_PLUGIN_DIR=extensions, gepinntes Base-Image-Digest (node:24-bookworm / bookworm-slim / Bun).
  • Stage 1: workspace-deps:26-45 — Kopiert nur die package.json-Liste, um den Main-Build-Layer vor Irrelevanz-Source-Änderungen zu schützen.
  • Stage 2: build:48-99 — COPY Bun binary, corepack enable, pnpm install --frozen-lockfile, matrix-sdk-crypto native addon Retry, pnpm build:docker + pnpm ui:build.
  • matrix-sdk-crypto retry:82-97 — 5-maliges Retry beim Download des native addon, da das CDN von matrix-sdk-crypto-nodejs manchmal instabil ist.
  • runtime-assets:135-154pnpm prune --prod --offline, entfernt Dev-Dependencies, d.ts, .map, node_modules/openclaw (Selbstreferenz).
  • Stage 3: runtime:156-189 — Base bookworm-slim, installiert ca-certificates curl git hostname lsof openssl procps python3 tini.
  • ca-certificates install:184-189 — Ohne dieses Paket scheitert HTTPS Outbound beim TLS-Handshake (error setting certificate file).
  • OPENCLAW_INSTALL_BROWSER:252-262 — Optionales Playwright Chromium + Xvfb, vermeidet 60–90s Install bei jedem Start.
  • OPENCLAW_INSTALL_DOCKER_CLI:268-301 — Optionales Docker CLI, für Sandbox-Container-Verwaltung; GPG-Fingerprint-Zweitprüfung verlangt exakt einen pub key.
  • CLI symlink:304-305ln -sf /app/openclaw.mjs /usr/local/bin/openclaw, vermeidet globales npm-Schreiben als non-root.
  • vorab angelegte Verzeichnisrechte:312-320install -d -m 0700 -o node -g node, danach stat -c verifiziert; verhindert, dass ein root-eigenes Verzeichnis beim ersten Mount eines named volume steckt.
  • ENV + USER + HEALTHCHECK + CMD:322-344NODE_ENV=production, USER node, HEALTHCHECK 3min-Intervall + tini-Eintritt + CMD ["node","openclaw.mjs","gateway"].
  • fly.tomlapp = "openclaw", primary_region = "iad", internal_port = 3000, auto_stop_machines = false (bleibt resident), min_machines_running = 1, shared-cpu-2x + 2GB, openclaw_data-Volume gemountet nach /data.
  • render.yamlruntime: docker, plan: starter, healthCheckPath: /health, OPENCLAW_GATEWAY_TOKEN: generateValue: true (auto-generiert), 1GB Disk nach /data.
  • docker-compose.ymlopenclaw-gateway + openclaw-cli; CLI nutzt network_mode: service:openclaw-gateway zum Teilen des Netzwerk-Namespace und mountet ~/.openclaw und das Secrets-Verzeichnis.

Datenfluss

Die letzten Schritte beim Container-Start (Dockerfile Ende:322-344) sind kritisch:

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", "--"] ist entscheidend — tini ist ein leichtgewichtiger Init, der Zombie-Prozesse (zombie process) reapt und Signale korrekt an den Node-Prozess weiterleitet. Im Container behandelt PID 1 default kein SIGCHLD; ohne tini würden ACP-/subagent-Subprozesse beim Beenden zu Zombies; tini leitet auch das SIGTERM von docker stop sauber an Node weiter, sodass das Gateway sauber beendet statt nach 10s SIGKILL zu bekommen.

HEALTHCHECK nutzt Nodes eingebautes fetch (Node 18+ global verfügbar), kein curl, ruft direkt /healthz. --start-period=15s gibt dem Gateway-Bootstrap Puffer (Plugin laden, Channel verbinden); 3-Minuten-Intervall ist rechtzeitig ohne Lärm. --retries=3 bedeutet, dass erst drei aufeinanderfolgende Fehlschläge den Container als unhealthy markieren — schützt vor gelegentlichen Jitter-Fehlurteilen.

docker-compose.yml ist beim Health-Check aggressiver: 30s-Intervall, 5 retries, 20s start_period (compose healthcheck). compose überschreibt default --bind lan, da bei Bridge-Networking ein Loopback-Bind vom Host aus nicht erreichbar ist — das ist die Falle, die im Dockerfile-Kommentar explizit gewarnt wird. compose fixiert auch die containerseitigen Umgebungsvariablenpfade:

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

Diese Überschreibung repariert #77436 — schreibt der Nutzer in .env z. B. OPENCLAW_STATE_DIR=/Users/alex/.openclaw, injiziert compose das in den Container, und der Node im Container versucht mkdir '/Users' und scheitert mit EACCES. Daher erzwingt compose die containerseitigen Pfade auf /home/node/.openclaw; der Host-Pfad dient nur als bind-mount source.

fly.toml (fly.toml) deployt dasselbe Image nach 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   # resident bleiben
auto_start_machines = true
min_machines_running = 1
processes = ["app"]

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

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

Fly nutzt internal_port = 3000 für Health-Probes (entspricht compose OPENCLAW_GATEWAY_PORT); force_https = true lässt Flys Edge TLS terminieren; auto_stop_machines = false + min_machines_running = 1 garantieren, dass WebSocket-Langverbindungen nicht durch Flys Scale-to-Zero gekillt werden — kritisch für Agent-System-Deployments: das Gateway muss resident bleiben, sonst reißen alle Clientsessions ab. NODE_OPTIONS = "--max-old-space-size=1536" passt zu 2GB VM-Speicher und lässt ~500MB für Bun/native addon/Playwright, vermeidet OOM-Kill.

Grenzen und Fehler

  • Base-Image-SHA-Pin: OPENCLAW_NODE_BOOKWORM_IMAGE/OPENCLAW_NODE_BOOKWORM_SLIM_IMAGE/OPENCLAW_BUN_IMAGE sind alle auf SHA256-Digest gepinnt (digest pin:12-17). Dependabot aktualisiert diese blessed digests — Release-Builds konsumieren geprüfte Snapshots statt jedes Mal den neuesten zu ziehen, garantiert reproduzierbaren Build.
  • matrix-sdk-crypto-Download-Retry (matrix-sdk-crypto retry:82-97): Das CDN des native addon von matrix-sdk-crypto-nodejs ist instabil — Exit 0 aber .node-Datei nicht heruntergeladen. Der Code macht 5 Retries + sleep $((attempt * 2)) exponentielles Backoff; schlägt es am Ende fehl, Exit 1. Diese Prüfung läuft nur, wenn OPENCLAW_EXTENSIONS matrix enthält.
  • A2UI Cross-Arch-Stub (a2ui stub:113-118): pnpm canvas:a2ui:bundle kann bei QEMU-Cross-Arch-Build (Apple Silicon baut amd64) fehlschlagen; CI baut nativ pro Architektur und ist nicht betroffen. Hier Fallback auf Stub (nicht fatal), damit lokaler Cross-Build am UI-Bundle nicht scheitert.
  • OPENCLAW_PREFER_PNPM=1 (OPENCLAW_PREFER_PNPM:124): UI-Build nutzt pnpm statt Bun, da Bun auf ARM-/Synology-Architekturen Kompatibilitätsprobleme hat.
  • prune --prod --offline (runtime-assets prune:140-154): Zuerst pnpm store add seed-tarball, dann --config.offline=true prune, damit prune nicht ins Netz muss. Nach prune validiert node scripts/check-package-dist-imports.mjs die Import-Grenzen und verhindert, dass Dev-only-Dependencies im Prod-Image auftauchen.
  • Non-root, aber /home/node/.config-Rechte-Falle (config dir ownership:309-320): install -d ohne -o node würde /home/node/.config als root:root erstellen; dann erbt das openclaw-Unterverzeichnis root, und der Container als node-Nutzer kann nicht schreiben (#85968). Der Code macht zuerst install -d -m 0755 -o node -g node /home/node/.config und legt dann das Unterverzeichnis an; stat -c verifiziert, dass die Rechte exakt passen.
  • Docker-CLI-GPG Single-Key-Erzwingung (docker gpg single key:283-289):install OPENCLAW_INSTALL_DOCKER_CLI=1 lädt den Docker-APT-Signing-Key und zählt per gpg --show-keys --with-colons die pub-Zeilen; verlangt exakt 1 — eine Datei mit mehreren Keys wird abgelehnt, um zu verhindern, dass „apt der ersten Key-Datei vertraut, aber tatsächlich einer anderen vertraut wurde".
  • bind-mount-Pfad-Leck (compose env override):host-.env mit macOS-Pfad darf nicht in den Container; compose überschreibt die vier Variablen OPENCLAW_STATE_DIR/OPENCLAW_CONFIG_PATH/OPENCLAW_CONFIG_DIR/OPENCLAW_WORKSPACE_DIR auf feste Pfade unter /home/node. Die bind-mount-source nutzt ${HOME}/.openclaw, hostseitig weiterhin konfigurierbar.
  • Fly-Residenz-Semantik: auto_stop_machines = false + min_machines_running = 1 ist eine harte Constraint für Agent-Gateway-Deployments — Fly tut default scale-to-zero zum Sparen, aber WebSocket-Langverbindungen reißen und alle IDE-/Client-Sessions gehen verloren. Fly-Deploy muss Auto-Stop explizit abschalten. shared-cpu-2x + 2GB ist die minimale vernünftige Konfiguration; darunter OOM-t der Node.
  • Render-Auto-generiertes Token: In render.yaml setzt OPENCLAW_GATEWAY_TOKEN: generateValue: true Render an, das Token automatisch zu generieren und zu injizieren — vermeidet, dass der Nutzer Token im yaml fest verdrahtet. healthCheckPath: /health nutzt den Alias statt /healthz, da die Render-Doku diesen Pfad bevorzugt.
  • cap_drop: [NET_RAW, NET_ADMIN] + no-new-privileges: Selbst ein kompromittierter Container kann keine Raw-Sockets bauen (schützt vor Port-Scan/ARP-Spoofing) und kein setuid-Privilege-Escalation. Das ist die Defense-in-Depth-Baseline des Container-Deployments; der Daemon-Modus erreicht diese Isolationsstufe nicht.

Zusammenfassung

Das Container-Deployment ersetzt launchd/systemd/schtasks durch Docker Engine/Fly machine/Render, aber die Verantwortungsaufteilung von OpenClaw bleibt: Der Gateway-Prozess läuft weiterhin node openclaw.mjs gateway (siehe Gateway-Kern), nur ist der Eintritt in tini gewickelt für Signale + Zombies, und der Health-Check läuft über die eingebauten /healthz//readyz-Endpunkte. Die drei Deploy-Ziele (Docker Compose lokal self-hosted / Fly.io Cloud / Render.com Cloud) teilen sich dasselbe Dockerfile; Unterschiede liegen nur in orchestratorspezifischem Volume-Mount, Ressourcenspez, Token-Injektion. Physische Maschine oder VM ohne Docker kann direkt über Daemon: Systemdienst als Systemdienst installiert werden; die Konfigurationsdatei (openclaw.json) ist in beiden Deploy-Formen identisch, siehe openclaw.json.