What you need
- flyctl CLI installed
- Fly.io account (free tier works)
- Model auth: API key for your chosen model provider
- Channel credentials: Discord bot token, Telegram token, etc.
Beginner quick path
- Clone repo, customize
fly.toml - Create app + volume, set secrets
- Deploy with
fly deploy - SSH in to create config, or use the Control UI
Create the Fly app
lhr (London), iad (Virginia), sjc (San Jose).Configure fly.toml
Edit The OpenClaw Docker image entrypoint is
fly.toml to match your app name and requirements. The repo’s tracked fly.toml is the public template shown below; deploy/fly.private.toml is the hardened, no-public-IP variant (see Private deployment).tini, running node openclaw.mjs gateway by default. Fly [processes] replaces the Docker CMD (here it runs node dist/index.js gateway ... directly, the same compiled entrypoint) without touching ENTRYPOINT, so the process still runs under tini.Key settings:| Setting | Why |
|---|---|
--bind lan | Binds to 0.0.0.0 so Fly’s proxy can reach the gateway |
--allow-unconfigured | Starts without a config file (you create one after) |
internal_port = 3000 | Must match --port 3000 (or OPENCLAW_GATEWAY_PORT) for Fly health checks |
memory = "2048mb" | 512MB is too small; 2GB recommended |
OPENCLAW_STATE_DIR = "/data" | Persists state on the volume |
Set secrets
--bind lan) require a valid gateway auth path. This example uses OPENCLAW_GATEWAY_TOKEN, but gateway.auth.password or a correctly configured non-loopback trusted-proxy deployment also satisfy the requirement. See Secrets management for the SecretRef contract.Treat these tokens like passwords. Prefer env vars/fly secrets over the config file for API keys and tokens so secrets stay out of openclaw.json.Deploy
gateway ready once the HTTP/WebSocket listener is up. Fly’s own health check watches internal_port = 3000 per fly.toml; the image’s Docker HEALTHCHECK directive additionally polls /healthz on its default port 18789, which is unused here since this deployment overrides the gateway to --port 3000.Create config file
SSH into the machine to create a proper config:With
OPENCLAW_STATE_DIR=/data, the config path is /data/openclaw.json.Replace https://my-openclaw.fly.dev with your real Fly app origin. Gateway startup seeds local Control UI origins from the runtime --bind and --port values so first boot can proceed before config exists, but browser access through Fly still needs the exact HTTPS origin listed in gateway.controlUi.allowedOrigins.The Discord token can come from either:- Environment variable
DISCORD_BOT_TOKEN(recommended for secrets); no need to add it to config, the gateway reads it automatically - Config file
channels.discord.token
Troubleshooting
”App is not listening on expected address”
The gateway is binding to127.0.0.1 instead of 0.0.0.0.
Fix: add --bind lan to your process command in fly.toml.
Health checks failing / connection refused
Fly cannot reach the gateway on the configured port. Fix: ensureinternal_port matches the gateway port (--port 3000 or OPENCLAW_GATEWAY_PORT=3000).
OOM / memory issues
Container keeps restarting or getting killed. Signs:SIGABRT, v8::internal::Runtime_AllocateInYoungGeneration, or silent restarts.
Fix: increase memory in fly.toml:
Gateway lock issues
Gateway refuses to start with “already running” errors after a container restart. The single-instance lock file lives at<tmpdir>/openclaw-<uid>/gateway.<hash>.lock (Linux: /tmp/openclaw-<uid>/gateway.<hash>.lock), not on the persistent /data volume, so a full container restart normally clears it along with the rest of the container filesystem. If the lock survives (for example a fly machine restart that preserves the container filesystem) and blocks startup, remove it manually:
Config not being read
--allow-unconfigured only bypasses the startup guard. It does not create or repair /data/openclaw.json, so make sure your real config exists and includes "gateway": { "mode": "local" } for a normal local gateway start.
Verify the config exists:
Writing config via SSH
fly ssh console -C does not support shell redirection. To write a config file:
fly sftp may fail if the file already exists; delete first:
State not persisting
If you lose auth profiles, channel/provider state, or sessions after a restart, the state dir is writing to the container filesystem instead of the volume. Fix: ensureOPENCLAW_STATE_DIR=/data is set in fly.toml and redeploy.
Updating
git pull + fly deploy is the supervised path here: it rebuilds the image from the Dockerfile, so the CLI/gateway version, the base OS image, and any Dockerfile changes all update together. openclaw update inside the running container is not the same operation, since the image ships as a Docker-built dist/ tree with no .git checkout and no npm-managed global install for it to detect; see Updating for that flow on VM-style installs.
Updating the machine command
To change the startup command without a full redeploy:fly deploy resets the machine command back to whatever is in fly.toml; re-apply manual changes after redeploying.
Private deployment (hardened)
By default, Fly allocates public IPs, so your gateway is reachable athttps://your-app.fly.dev and discoverable by internet scanners (Shodan, Censys, etc.).
Use deploy/fly.private.toml for a hardened deployment with no public IP: it omits [http_service], so no public ingress is allocated.
When to use private deployment
- Only outbound calls/messages (no inbound webhooks)
- ngrok or Tailscale tunnels handle any webhook callbacks
- Gateway access is via SSH, proxy, or WireGuard instead of a browser
- The deployment should be hidden from internet scanners
Setup
fly ips list should show only a private type IP:
Accessing a private deployment
Option 1: local proxy (simplest)Webhooks with private deployment
For webhook callbacks (Twilio, Telnyx, etc.) without public exposure:- ngrok tunnel: run ngrok inside the container, or as a sidecar
- Tailscale Funnel: expose specific paths via Tailscale
- Outbound-only: some providers (Twilio) work for outbound calls without webhooks
plugins.entries.voice-call.config:
webhookSecurity.allowedHosts to the tunnel hostname so forwarded host headers are accepted.
Security tradeoffs
| Aspect | Public | Private |
|---|---|---|
| Internet scanners | Discoverable | Hidden |
| Direct attacks | Possible | Blocked |
| Control UI access | Browser | Proxy/VPN |
| Webhook delivery | Direct | Via tunnel |
Notes
- Fly.io uses x86 architecture; the Dockerfile is compatible with both x86 and ARM.
- For WhatsApp/Telegram onboarding, use
fly ssh console. - Persistent data lives on the volume at
/data. - Signal requires signal-cli (a Java-based CLI) on the image; use a custom image and keep memory at 2GB+.
Cost
With the recommended config (shared-cpu-2x, 2GB RAM), expect roughly $10-15/month depending on usage; the free tier covers some baseline allowance. See Fly.io pricing for current rates.
Next steps
- Set up messaging channels: Channels
- Configure the Gateway: Gateway configuration
- Keep OpenClaw up to date: Updating