Despliegue con Docker y Fly
Responsabilidad
El Dockerfile (Dockerfile CMD:343-344) es la entrada raíz del despliegue containerizado (containerization) de OpenClaw: un multi-stage build produce una imagen runtime mínima que por defecto arranca node openclaw.mjs gateway. fly.toml (fly.toml) es el descriptor de despliegue en Fly.io, montando la imagen Docker en una vm Fly + volumen persistente. render.yaml (render.yaml) es la config equivalente en Render.com. docker-compose.yml (docker-compose.yml) orquesta dos servicios gateway + CLI, útil tanto para dev local como para self-host.
Esta forma de despliegue es complementaria a Daemon: servicio del sistema: dentro del contenedor no hay launchd/systemd/schtasks, el orquestador (Docker Engine / Fly machine / Render) es el «gestor de servicios del sistema», responsable del auto-arranque y reinicio tras crash. OpenClaw solo necesita exponer un endpoint de health check + aceptar la señal estándar (SIGTERM) para una salida limpia, el resto al orquestador.
Motivación de diseño
¿Por qué separar el despliegue containerizado en su propia capa?
- Imagen mínima: la imagen completa
node:24-bookwormpesa 1GB+, pero el runtime solo necesitanode:24-bookworm-slim+ unos pocos paquetes de sistema comoca-certificates/curl/git/tini. El multi-stage build separa la capa de build (Bun/pnpm install +pnpm build:docker+pnpm ui:build) de la capa de runtime; la imagen final no lleva Bun, ni código fuente, ni dev dependencies, ni .d.ts/.map (prune d.ts and maps:149). - Defaults seguros:
USER node(USER node:327), corre como non-root;cap_drop: [NET_RAW, NET_ADMIN]+security_opt: [no-new-privileges:true](cap_drop), el contenedor no puede abrir raw sockets ni escalar privilegios.Umask=0o077protege permisos de archivos en modo daemon; en contenedor se logra el mismo efecto pre-creando directorios0700(install -d permissions:312-320). - Health check integrado:
HEALTHCHECK(HEALTHCHECK:341-342) invoca directamente/healthz, sin depender de scripts de probing externos — el orquestador (Docker Compose/Fly/Render) lo usa para decidir si el contenedor vive./healthzes liveness,/readyzes readiness, con alias/healthy/readypor compatibilidad. - Multi-orquestador:
fly.toml,render.yaml,docker-compose.ymlson tres configs que cubren tres destinos de despliegue, pero por debajo todas usan la misma imagen — las diferencias están solo en cómo se declaran puertos, montajes y variables de entorno.
Archivos clave
Dockerfile build args:10-25—OPENCLAW_EXTENSIONS="",OPENCLAW_BUNDLED_PLUGIN_DIR=extensions, base image digest pinned (node:24-bookworm / bookworm-slim / Bun).Stage 1: workspace-deps:26-45— solo copia la lista de package.json, evita que la capa main build se invalidate por cambios de código fuente irrelevantes.Stage 2: build:48-99— COPY del binario Bun,corepack enable,pnpm install --frozen-lockfile, reintentos del native addon matrix-sdk-crypto,pnpm build:docker+pnpm ui:build.matrix-sdk-crypto retry:82-97— 5 reintentos para descargar el native addon, porque el CDN de matrix-sdk-crypto-nodejs a veces es inestable.runtime-assets:135-154—pnpm prune --prod --offline, elimina dev dependencies, .d.ts, .map,node_modules/openclaw(auto-referencia).Stage 3: runtime:156-189— basebookworm-slim, instalaca-certificates curl git hostname lsof openssl procps python3 tini.ca-certificates install:184-189— sin este paquete el HTTPS outbound falla en TLS handshake (error setting certificate file).OPENCLAW_INSTALL_BROWSER:252-262— Playwright Chromium + Xvfb opcional, evita un install de 60-90s en cada arranque.OPENCLAW_INSTALL_DOCKER_CLI:268-301— Docker CLI opcional, necesario para sandbox con gestión de contenedores; fingerprint GPG con doble validación, exige exactamente una pub key.CLI symlink:304-305—ln -sf /app/openclaw.mjs /usr/local/bin/openclaw, evita el npm global write como non-root.pre-create dir perms:312-320—install -d -m 0700 -o node -g node, luegostat -cpara verificar, previene que un directorio owned by root contamine el named volume al montar por primera vez.ENV + USER + HEALTHCHECK + CMD:322-344—NODE_ENV=production,USER node,HEALTHCHECK3min interval + entrypointtini+CMD ["node","openclaw.mjs","gateway"].fly.toml—app = "openclaw",primary_region = "iad",internal_port = 3000,auto_stop_machines = false(mantiene residente),min_machines_running = 1,shared-cpu-2x+ 2GB, volumeopenclaw_datamontado en/data.render.yaml—runtime: docker,plan: starter,healthCheckPath: /health,OPENCLAW_GATEWAY_TOKEN: generateValue: true(autogenerado), 1GB disk montado en/data.docker-compose.yml— dos serviciosopenclaw-gateway+openclaw-cli, CLI usanetwork_mode: service:openclaw-gatewaypara compartir network namespace, monta~/.openclawy el directorio de secrets.
Flujo de datos
Los últimos pasos del arranque del contenedor (Dockerfile tail:322-344) son clave:
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", "--"] es clave — tini es un init ligero que se encarga de reap zombies (zombie process) y reenvía señales correctamente al proceso Node. En un contenedor, PID 1 por defecto no maneja SIGCHLD; sin tini, los subprocesos ACP/subagent al salir se convertirían en zombies. tini también reenvía limpiamente el SIGTERM de docker stop a Node, dejando que el gateway haga una salida limpia en lugar de recibir SIGKILL a los 10s.
HEALTHCHECK usa el fetch builtin de Node (global desde Node 18), no necesita curl, invoca directamente /healthz. --start-period=15s da margen al bootstrap del gateway (cargar plugins, conectar canales); un probe cada 3 minutos es suficientemente oportuno sin ser ruidoso. --retries=3 significa que 3 fallos consecutivos marcan unhealthy, evitando falsos positivos por jitter.
El health check de docker-compose.yml es más agresivo: intervalo 30s, 5 retries, 20s start_period (compose healthcheck). El compose además override --bind lan por defecto, porque en bridge networking el bind a loopback no es accesible desde el host — es el pozo que el comentario del Dockerfile advierte explícitamente. Compose también fija los paths de variables de entorno del lado contenedor:
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/workspaceEste override arregla #77436 — el usuario escribe OPENCLAW_STATE_DIR=/Users/alex/.openclaw en .env, compose lo inyecta al contenedor, y el Node del contenedor intenta mkdir '/Users' y falla con EACCES. Por eso compose fuerza el override de estos paths del lado contenedor a /home/node/.openclaw; el path del host solo se usa como bind-mount source.
fly.toml (fly.toml) despliega la misma imagen a 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 # mantiene residente
auto_start_machines = true
min_machines_running = 1
processes = ["app"]
[[vm]]
size = "shared-cpu-2x"
memory = "2048mb"
[mounts]
source = "openclaw_data"
destination = "/data"Fly usa internal_port = 3000 para el health probe (corresponde al OPENCLAW_GATEWAY_PORT del compose), force_https = true deja al edge de Fly terminar TLS, auto_stop_machines = false + min_machines_running = 1 garantiza que las conexiones WebSocket largas no se corten por el scale-to-zero de Fly — esto es clave en un despliegue de agent: el gateway debe ser residente; si se cae, todas las sesiones de IDE/clientes se pierden. NODE_OPTIONS = "--max-old-space-size=1536" con 2GB de RAM de vm deja ~500MB para Bun/native addon/Playwright, evitando OOM kill.
Límites y fallos
- Base image SHA pin:
OPENCLAW_NODE_BOOKWORM_IMAGE/OPENCLAW_NODE_BOOKWORM_SLIM_IMAGE/OPENCLAW_BUN_IMAGEtodos con pin al digest SHA256 (digest pin:12-17). Dependabot actualiza estos digests blessed — el build de release consume snapshots auditados en lugar de tirar latest cada vez, garantizando reproducible build. - Reintentos de descarga matrix-sdk-crypto (
matrix-sdk-crypto retry:82-97): el CDN del native addon de matrix-sdk-crypto-nodejs es inestable, puede exit 0 sin que el archivo.nodese haya descargado. El código hace 5 reintentos +sleep $((attempt * 2))backoff exponencial, y si al final sigue fallando exit 1. Solo se hace esta comprobación siOPENCLAW_EXTENSIONSincluyematrix. - A2UI cross-arch stub (
a2ui stub:113-118):pnpm canvas:a2ui:bundlepuede fallar en builds QEMU cross-arch (Apple Silicon construyendo amd64), el CI construye nativamente cada arch así que no le afecta. Aquí se cae a un stub (no fatal), dejando que el cross-build local no se caiga por el bundle UI. OPENCLAW_PREFER_PNPM=1(OPENCLAW_PREFER_PNPM:124): el UI build usa pnpm en lugar de Bun, Bun tiene problemas de compat en ARM/Synology.prune --prod --offline(runtime-assets prune:140-154): primeropnpm store addpara seedear tarballs, luego--config.offline=trueprune, evita tener que ir a red durante el prune. Tras prune se ejecutanode scripts/check-package-dist-imports.mjspara validar los bounds de import, previene que aparezcan dependencias dev-only en la imagen prod.- Non-root pero pozo de permisos en
/home/node/.config(config dir ownership:309-320):install -dsin-o nodecrea/home/node/.configcomo root:root, luego los subdirectoriosopenclawheredan root, y el contenedor corriendo comonodeno puede escribir (#85968). El código primero haceinstall -d -m 0755 -o node -g node /home/node/.configy luego crea subdirectorios, validando constat -cque los permisos cumplen. - Docker CLI GPG single key enforcement (
docker gpg single key:283-289): cuandoOPENCLAW_INSTALL_DOCKER_CLI=1, tras descargar la Docker apt signing key se corregpg --show-keys --with-colonspara contar líneaspub, exigiendo exactamente 1 — archivos multi-key se rechazan, previniendo «apt confía en la primera key del archivo pero la que realmente importa es otra». - Bind-mount path leak (
compose env override): los paths macOS del.envdel host no pueden entrar al contenedor, así que compose fuerza el override de cuatro variables —OPENCLAW_STATE_DIR/OPENCLAW_CONFIG_PATH/OPENCLAW_CONFIG_DIR/OPENCLAW_WORKSPACE_DIR— a paths fijos bajo/home/node. El bind-mount source usa${HOME}/.openclaw, el lado host se puede seguir especificando. - Semántica residente de Fly:
auto_stop_machines = false+min_machines_running = 1es un hard requirement para desplegar un agent gateway — Fly por defecto escala a cero para ahorrar, pero una conexión WebSocket larga cortada pierde todas las sesiones de IDE/clientes. El despliegue en Fly debe desactivar explícitamente el auto-stop.shared-cpu-2x+ 2GB es la config mínima razonable; por debajo de esa memoria Node se va a OOM. - Render autogenera token: en
render.yaml,OPENCLAW_GATEWAY_TOKEN: generateValue: truehace que Render autogenere e inyecte — evita que el usuario hardcodee token en yaml.healthCheckPath: /healthusa el alias en lugar de/healthz, más común en la documentación de Render. cap_drop: [NET_RAW, NET_ADMIN]+no-new-privileges: aunque el contenedor se compromise, no puede abrir raw sockets (previene port scanning/ARP spoofing) ni escalar privilegios consetuid. Es la baseline de defense-in-depth del despliegue containerizado; el modo daemon no puede alcanzar este nivel de aislamiento.
Resumen
El despliegue containerizado sustituye launchd/systemd/schtasks por Docker Engine/Fly machine/Render, pero la división de responsabilidades de OpenClaw no cambia: el proceso gateway sigue corriendo node openclaw.mjs gateway (ver Núcleo del gateway), solo que el entrypoint envuelve tini para señales + zombies, y el health check se apoya en los endpoints builtin /healthz//readyz. Los tres destinos de despliegue (Docker Compose local self-host / Fly.io cloud / Render.com cloud) comparten el mismo Dockerfile, con diferencias solo en cómo el orquestador monta volúmenes, define recursos e inyecta tokens. Para despliegue en bare metal o VM sin Docker se puede ir por Daemon: servicio del sistema e instalar directamente como servicio del sistema; el archivo de config (openclaw.json) es el mismo en ambas formas, ver openclaw.json.