Docker- und Fly-Deployment
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?
- Image minimal: Das vollständige
node:24-bookworm-Image ist über 1 GB groß, aber zur Laufzeit reichennode:24-bookworm-slim+ wenige Systempakete wieca-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). - 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=0o077schützt im Daemon-Modus die Dateirechte; im Container wird dasselbe durch vorab angelegte0700-Verzeichnisse erreicht (install -d permissions:312-320). - 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./healthzist Liveness,/readyzReadiness; die Aliase/healthund/readydecken Gewohnheiten ab. - 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-25—OPENCLAW_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-154—pnpm prune --prod --offline, entfernt Dev-Dependencies, d.ts, .map,node_modules/openclaw(Selbstreferenz).Stage 3: runtime:156-189— Basebookworm-slim, installiertca-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-305—ln -sf /app/openclaw.mjs /usr/local/bin/openclaw, vermeidet globales npm-Schreiben als non-root.vorab angelegte Verzeichnisrechte:312-320—install -d -m 0700 -o node -g node, danachstat -cverifiziert; verhindert, dass ein root-eigenes Verzeichnis beim ersten Mount eines named volume steckt.ENV + USER + HEALTHCHECK + CMD:322-344—NODE_ENV=production,USER node,HEALTHCHECK3min-Intervall +tini-Eintritt +CMD ["node","openclaw.mjs","gateway"].fly.toml—app = "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.yaml—runtime: docker,plan: starter,healthCheckPath: /health,OPENCLAW_GATEWAY_TOKEN: generateValue: true(auto-generiert), 1GB Disk nach/data.docker-compose.yml—openclaw-gateway+openclaw-cli; CLI nutztnetwork_mode: service:openclaw-gatewayzum Teilen des Netzwerk-Namespace und mountet~/.openclawund das Secrets-Verzeichnis.
Datenfluss
Die letzten Schritte beim Container-Start (Dockerfile Ende:322-344) sind kritisch:
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:
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/workspaceDiese Ü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:
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_IMAGEsind 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, wennOPENCLAW_EXTENSIONSmatrixenthält. - A2UI Cross-Arch-Stub (
a2ui stub:113-118):pnpm canvas:a2ui:bundlekann 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): Zuerstpnpm store addseed-tarball, dann--config.offline=trueprune, damit prune nicht ins Netz muss. Nach prune validiertnode scripts/check-package-dist-imports.mjsdie 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 -dohne-o nodewürde/home/node/.configals root:root erstellen; dann erbt dasopenclaw-Unterverzeichnis root, und der Container alsnode-Nutzer kann nicht schreiben (#85968). Der Code macht zuerstinstall -d -m 0755 -o node -g node /home/node/.configund legt dann das Unterverzeichnis an;stat -cverifiziert, dass die Rechte exakt passen. - Docker-CLI-GPG Single-Key-Erzwingung (
docker gpg single key:283-289):install OPENCLAW_INSTALL_DOCKER_CLI=1lädt den Docker-APT-Signing-Key und zählt pergpg --show-keys --with-colonsdiepub-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-.envmit macOS-Pfad darf nicht in den Container; compose überschreibt die vier VariablenOPENCLAW_STATE_DIR/OPENCLAW_CONFIG_PATH/OPENCLAW_CONFIG_DIR/OPENCLAW_WORKSPACE_DIRauf 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 = 1ist 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.yamlsetztOPENCLAW_GATEWAY_TOKEN: generateValue: trueRender an, das Token automatisch zu generieren und zu injizieren — vermeidet, dass der Nutzer Token im yaml fest verdrahtet.healthCheckPath: /healthnutzt 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 keinsetuid-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.