Despliegue en Fly.io

Objetivo: Gateway de OpenClaw ejecutándose en una máquina Fly.io con almacenamiento persistente, HTTPS automático y acceso a Discord/canales.

Qué necesitas

  • flyctl CLI instalado
  • Cuenta de Fly.io (el tier gratuito funciona)
  • Auth del modelo: API key del proveedor elegido
  • Credenciales de canal: token de bot Discord, token de Telegram, etc.

Ruta rápida para principiantes

  1. Clonar repo → personalizar fly.toml
  2. Crear app + volumen → configurar secrets
  3. Desplegar con fly deploy
  4. Conectar por SSH para crear config o usar el Control UI

1) Crear la app de Fly

# Clonar el repo
git clone https://github.com/openclaw/openclaw.git
cd openclaw

# Crear una nueva app de Fly (elige tu propio nombre)
fly apps create my-openclaw

# Crear un volumen persistente (1GB suele ser suficiente)
fly volumes create openclaw_data --size 1 --region iad

Consejo: Elige una región cercana a ti. Opciones comunes: lhr (Londres), iad (Virginia), sjc (San José).

2) Configurar fly.toml

Edita fly.toml para que coincida con el nombre de tu app y tus requisitos.

Nota de seguridad: La configuración por defecto expone una URL pública. Para un despliegue hardened sin IP pública, consulta Despliegue privado o usa fly.private.toml.

app = "my-openclaw"  # Nombre de tu app
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
  auto_start_machines = true
  min_machines_running = 1
  processes = ["app"]

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

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

Configuraciones clave:

ConfiguraciónPor qué
--bind lanSe enlaza a 0.0.0.0 para que el proxy de Fly pueda alcanzar el gateway
--allow-unconfiguredInicia sin un archivo de configuración (lo crearás después)
internal_port = 3000Debe coincidir con --port 3000 (o OPENCLAW_GATEWAY_PORT) para los health checks de Fly
memory = "2048mb"512MB es muy poco; se recomiendan 2GB
OPENCLAW_STATE_DIR = "/data"Persiste el estado en el volumen

3) Configurar secrets

# Requerido: Token del gateway (para binding no-loopback)
fly secrets set OPENCLAW_GATEWAY_TOKEN=$(openssl rand -hex 32)

# API keys del proveedor de modelos
fly secrets set ANTHROPIC_API_KEY=sk-ant-...

# Opcional: Otros proveedores
fly secrets set OPENAI_API_KEY=sk-...
fly secrets set GOOGLE_API_KEY=...

# Tokens de canales
fly secrets set DISCORD_BOT_TOKEN=MTQ...

Notas:

  • Los bindings no-loopback (--bind lan) requieren OPENCLAW_GATEWAY_TOKEN por seguridad.
  • Trata estos tokens como contraseñas.
  • Prefiere variables de entorno sobre el archivo de config para todas las API keys y tokens. Esto mantiene los secretos fuera de openclaw.json donde podrían ser accidentalmente expuestos o registrados en logs.

4) Desplegar

fly deploy

El primer despliegue compila la imagen Docker (~2-3 minutos). Los despliegues posteriores son más rápidos.

Después del despliegue, verifica:

fly status
fly logs

Deberías ver:

[gateway] listening on ws://0.0.0.0:3000 (PID xxx)
[discord] logged in to discord as xxx

5) Crear archivo de configuración

Conéctate por SSH a la máquina para crear una configuración adecuada:

fly ssh console

Crea el directorio y archivo de configuración:

mkdir -p /data
cat > /data/openclaw.json << 'EOF'
{
  "agents": {
    "defaults": {
      "model": {
        "primary": "anthropic/claude-opus-4-6",
        "fallbacks": ["anthropic/claude-sonnet-4-5", "openai/gpt-4o"]
      },
      "maxConcurrent": 4
    },
    "list": [
      {
        "id": "main",
        "default": true
      }
    ]
  },
  "auth": {
    "profiles": {
      "anthropic:default": { "mode": "token", "provider": "anthropic" },
      "openai:default": { "mode": "token", "provider": "openai" }
    }
  },
  "bindings": [
    {
      "agentId": "main",
      "match": { "channel": "discord" }
    }
  ],
  "channels": {
    "discord": {
      "enabled": true,
      "groupPolicy": "allowlist",
      "guilds": {
        "YOUR_GUILD_ID": {
          "channels": { "general": { "allow": true } },
          "requireMention": false
        }
      }
    }
  },
  "gateway": {
    "mode": "local",
    "bind": "auto"
  },
  "meta": {
    "lastTouchedVersion": "2026.1.29"
  }
}
EOF

Nota: Con OPENCLAW_STATE_DIR=/data, la ruta de configuración es /data/openclaw.json.

Nota: El token de Discord puede venir de:

  • Variable de entorno: DISCORD_BOT_TOKEN (recomendado para secretos)
  • Archivo de configuración: channels.discord.token

Si usas la variable de entorno, no necesitas agregar el token a la config. El gateway lee DISCORD_BOT_TOKEN automáticamente.

Reinicia para aplicar:

exit
fly machine restart <machine-id>

6) Acceder al Gateway

Control UI

Abrir en el navegador:

fly open

O visita https://my-openclaw.fly.dev/

Pega tu token del gateway (el de OPENCLAW_GATEWAY_TOKEN) para autenticarte.

Logs

fly logs              # Logs en vivo
fly logs --no-tail    # Logs recientes

Consola SSH

fly ssh console

Troubleshooting

”App is not listening on expected address”

El gateway se está enlazando a 127.0.0.1 en vez de 0.0.0.0.

Solución: Agrega --bind lan al comando del proceso en fly.toml.

Health checks fallando / conexión rechazada

Fly no puede alcanzar el gateway en el puerto configurado.

Solución: Asegúrate de que internal_port coincida con el puerto del gateway (configura --port 3000 o OPENCLAW_GATEWAY_PORT=3000).

OOM / Problemas de memoria

El contenedor sigue reiniciándose o siendo terminado. Señales: SIGABRT, v8::internal::Runtime_AllocateInYoungGeneration, o reinicios silenciosos.

Solución: Aumenta la memoria en fly.toml:

[[vm]]
  memory = "2048mb"

O actualiza una máquina existente:

fly machine update <machine-id> --vm-memory 2048 -y

Nota: 512MB es muy poco. 1GB puede funcionar pero puede caer por OOM bajo carga o con logging verbose. Se recomiendan 2GB.

Problemas con el lock del Gateway

El gateway se niega a iniciar con errores “already running”.

Esto ocurre cuando el contenedor se reinicia pero el archivo de lock PID persiste en el volumen.

Solución: Elimina el archivo de lock:

fly ssh console --command "rm -f /data/gateway.*.lock"
fly machine restart <machine-id>

El archivo de lock está en /data/gateway.*.lock (no en un subdirectorio).

Config no se lee

Si usas --allow-unconfigured, el gateway crea una config mínima. Tu config personalizada en /data/openclaw.json debería leerse al reiniciar.

Verifica que la config existe:

fly ssh console --command "cat /data/openclaw.json"

Escribir Config vía SSH

El comando fly ssh console -C no soporta redirección de shell. Para escribir un archivo de config:

# Usar echo + tee (pipe de local a remoto)
echo '{"your":"config"}' | fly ssh console -C "tee /data/openclaw.json"

# O usar sftp
fly sftp shell
> put /local/path/config.json /data/openclaw.json

Nota: fly sftp puede fallar si el archivo ya existe. Elimínalo primero:

fly ssh console --command "rm /data/openclaw.json"

Estado no persiste

Si pierdes credenciales o sesiones después de un reinicio, el directorio de estado está escribiendo en el filesystem del contenedor.

Solución: Asegúrate de que OPENCLAW_STATE_DIR=/data esté configurado en fly.toml y redespliega.

Actualizaciones

# Obtener últimos cambios
git pull

# Redesplegar
fly deploy

# Verificar salud
fly status
fly logs

Actualizar comando de la máquina

Si necesitas cambiar el comando de arranque sin un redeploy completo:

# Obtener ID de la máquina
fly machines list

# Actualizar comando
fly machine update <machine-id> --command "node dist/index.js gateway --port 3000 --bind lan" -y

# O con aumento de memoria
fly machine update <machine-id> --vm-memory 2048 --command "node dist/index.js gateway --port 3000 --bind lan" -y

Nota: Después de fly deploy, el comando de la máquina puede restablecerse a lo que está en fly.toml. Si hiciste cambios manuales, vuelve a aplicarlos después del deploy.

Despliegue privado (Hardened)

Por defecto, Fly asigna IPs públicas, haciendo tu gateway accesible en https://your-app.fly.dev. Es conveniente, pero significa que tu despliegue es descubrible por escáneres de internet (Shodan, Censys, etc.).

Para un despliegue hardened sin exposición pública, usa la plantilla privada.

Cuándo usar despliegue privado

  • Solo haces llamadas/mensajes salientes (sin webhooks entrantes)
  • Usas túneles ngrok o Tailscale para callbacks de webhooks
  • Accedes al gateway vía SSH, proxy o WireGuard en vez de navegador
  • Quieres que el despliegue esté oculto de escáneres de internet

Configuración

Usa fly.private.toml en vez de la config estándar:

# Desplegar con config privada
fly deploy -c fly.private.toml

O convierte un despliegue existente:

# Listar IPs actuales
fly ips list -a my-openclaw

# Liberar IPs públicas
fly ips release <public-ipv4> -a my-openclaw
fly ips release <public-ipv6> -a my-openclaw

# Cambiar a config privada para que futuros deploys no reasignen IPs públicas
# (elimina [http_service] o despliega con la plantilla privada)
fly deploy -c fly.private.toml

# Asignar IPv6 solo privada
fly ips allocate-v6 --private -a my-openclaw

Después de esto, fly ips list debería mostrar solo una IP de tipo private:

VERSION  IP                   TYPE             REGION
v6       fdaa:x:x:x:x::x      private          global

Acceder a un despliegue privado

Como no hay URL pública, usa uno de estos métodos:

Opción 1: Proxy local (el más simple)

# Reenviar puerto local 3000 a la app
fly proxy 3000:3000 -a my-openclaw

# Luego abre http://localhost:3000 en el navegador

Opción 2: WireGuard VPN

# Crear config de WireGuard (una sola vez)
fly wireguard create

# Importar al cliente WireGuard, luego acceder vía IPv6 interna
# Ejemplo: http://[fdaa:x:x:x:x::x]:3000

Opción 3: Solo SSH

fly ssh console -a my-openclaw

Webhooks con despliegue privado

Si necesitas callbacks de webhooks (Twilio, Telnyx, etc.) sin exposición pública:

  1. Túnel ngrok - Ejecuta ngrok dentro del contenedor o como sidecar
  2. Tailscale Funnel - Expone rutas específicas vía Tailscale
  3. Solo saliente - Algunos proveedores (Twilio) funcionan bien para llamadas salientes sin webhooks

Ejemplo de config de llamadas de voz con ngrok:

{
  "plugins": {
    "entries": {
      "voice-call": {
        "enabled": true,
        "config": {
          "provider": "twilio",
          "tunnel": { "provider": "ngrok" },
          "webhookSecurity": {
            "allowedHosts": ["example.ngrok.app"]
          }
        }
      }
    }
  }
}

El túnel ngrok se ejecuta dentro del contenedor y proporciona una URL pública de webhook sin exponer la app de Fly en sí. Configura webhookSecurity.allowedHosts con el hostname público del túnel para que los headers de host reenviados sean aceptados.

Beneficios de seguridad

AspectoPúblicoPrivado
Escáneres de internetDescubribleOculto
Ataques directosPosiblesBloqueados
Acceso al Control UINavegadorProxy/VPN
Entrega de webhooksDirectoVía túnel

Notas

  • Fly.io usa arquitectura x86 (no ARM)
  • El Dockerfile es compatible con ambas arquitecturas
  • Para onboarding de WhatsApp/Telegram, usa fly ssh console
  • Los datos persistentes viven en el volumen en /data
  • Signal requiere Java + signal-cli; usa una imagen personalizada y mantén la memoria en 2GB+.

Costo

Con la configuración recomendada (shared-cpu-2x, 2GB RAM):

  • ~$10-15/mes dependiendo del uso
  • El tier gratuito incluye algo de asignación

Consulta precios de Fly.io para más detalles.