Platform setup
How to Deploy OpenClaw on Fly.io
Browse more in Platform setup.
All platform setup guides →io using the official Docker setup and a persistent volume. You wire up Fly Machines, storage, and secrets so OpenClaw can talk to your model providers and Discord.
io with HTTPS, logs, and an editable config on disk.
Prerequisites
- ✓flyctl CLI installed on your machine (see Fly.io docs for install instructions).
- ✓A Fly.io account (free tier works).
- ✓At least one model provider API key, such as an ANTHROPIC_API_KEY or OPENAI_API_KEY.
- ✓Channel credentials if you want chat integration, for example a DISCORD_BOT_TOKEN.
Steps
- 1
Clone the OpenClaw repo and create your Fly app
Start from the official OpenClaw repo so you get the Dockerfile and Fly config templates. Then create a Fly app and a persistent volume that OpenClaw uses for state and config; picking a nearby region keeps latency down.
bash# Clone the repo git clone https://github.com/openclaw/openclaw.git cd openclaw # Create a new Fly app (pick your own name) fly apps create my-openclaw # Create a persistent volume (1GB is usually enough) fly volumes create openclaw_data --size 1 --region iad - 2
Configure fly.toml for the OpenClaw gateway
toml. 0, matching the internal port, and pointing state at the mounted volume so config and sessions survive restarts.
tomlapp = "my-openclaw" # Your app name 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" - 3
Set gateway, model, and channel secrets on Fly
json. You need a gateway token when binding to lan, plus API keys for your model providers and any channel tokens like Discord.
bash# Required: Gateway token (for non-loopback binding) fly secrets set OPENCLAW_GATEWAY_TOKEN=$(openssl rand -hex 32) # Model provider API keys fly secrets set ANTHROPIC_API_KEY=sk-ant-... # Optional: Other providers fly secrets set OPENAI_API_KEY=sk-... fly secrets set GOOGLE_API_KEY=... # Channel tokens fly secrets set DISCORD_BOT_TOKEN=MTQ... - 4
Deploy OpenClaw to Fly.io and verify it started
Deploy builds the Docker image and boots a Fly Machine with your config. After the first deploy, check status and logs to confirm the gateway is listening and your Discord bot (or other channels) logged in.
bashfly deploy fly status fly logs [gateway] listening on ws://0.0.0.0:3000 (PID xxx) [discord] logged in to discord as xxx - 5
Create the OpenClaw config file on the Fly volume
json on the /data volume for agents, models, and channels. SSH into the machine, write the config file, then restart the machine so the gateway picks it up.
bashfly ssh console mkdir -p /data cat > /data/openclaw.json << 'EOF' { "agents": { "defaults": { "model": { "primary": "anthropic/claude-opus-4-6", "fallbacks": ["anthropic/claude-sonnet-4-6", "openai/gpt-5.4"] }, "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": {} } EOF exit fly machine restart <machine-id> - 6
Open the Control UI and inspect logs and SSH access
Once the gateway is healthy, use Fly’s helpers to reach the Control UI and debug issues. fly open hits the HTTPS endpoint, fly logs gives you live output, and fly ssh console lets you poke around the container and volume.
bashfly open fly logs # Live logs fly logs --no-tail # Recent logs fly ssh console - 7
Optionally harden with a private-only Fly deployment
dev URL, switch to the private template so the app only has a private IPv6 address. You can then reach it via fly proxy, WireGuard, or SSH while keeping it off internet scanners.
bash# Deploy with private config fly deploy -c fly.private.toml # List current IPs fly ips list -a my-openclaw # Release public IPs fly ips release <public-ipv4> -a my-openclaw fly ips release <public-ipv6> -a my-openclaw # Switch to private config so future deploys don't re-allocate public IPs # (remove [http_service] or deploy with the private template) fly deploy -c fly.private.toml # Allocate private-only IPv6 fly ips allocate-v6 --private -a my-openclaw # Forward local port 3000 to the app fly proxy 3000:3000 -a my-openclaw # Create WireGuard config (one-time) fly wireguard create # SSH only fly ssh console -a my-openclaw - 8
Update your OpenClaw deployment over time
When you pull new OpenClaw versions or need to tweak the machine command or memory, use Fly’s update flow instead of recreating everything. This keeps your volume and state intact while rolling out changes.
bash# Pull latest changes git pull # Redeploy fly deploy # Check health fly status fly logs # Get machine ID fly machines list # Update command fly machine update <machine-id> --command "node dist/index.js gateway --port 3000 --bind lan" -y # Or with memory increase fly machine update <machine-id> --vm-memory 2048 --command "node dist/index.js gateway --port 3000 --bind lan" -y
Configuration
| Option | Description | Example |
|---|---|---|
| NODE_ENV | Sets the Node.js environment mode for the OpenClaw process. | production |
| OPENCLAW_PREFER_PNPM | Tells OpenClaw to prefer pnpm for package management inside the container. | 1 |
| OPENCLAW_STATE_DIR | Directory where OpenClaw stores state and config so it persists on the Fly volume. | /data |
| NODE_OPTIONS | Configures Node.js runtime options such as the maximum old-space heap size. | --max-old-space-size=1536 |
| OPENCLAW_GATEWAY_TOKEN | Shared secret token required when the gateway binds to a non-loopback address. | 7f3c2a9e4b1d8c6f0e2a9b3c5d7e1f09c3a4b6d8e0f1a2c3d4e5f6a7b8c9d0e |
| ANTHROPIC_API_KEY | Anthropic API key used for Claude model calls in your OpenClaw agents. | sk-ant-abc1234567890 |
| OPENAI_API_KEY | OpenAI API key used for OpenAI-backed models configured in your agents. | sk-abc1234567890 |
| GOOGLE_API_KEY | Google API key used when you enable Google-based model providers. | AIzaSyA-abc1234567890 |
| DISCORD_BOT_TOKEN | Discord bot token that the gateway uses to log in and handle Discord messages. | MTQ1Njc4OTAxMjM0NTY3ODkw.XYZ |
| gateway.mode | Controls how the gateway runs; local mode starts a normal local gateway. | local |
| gateway.bind | Binding mode for the gateway inside openclaw.json; auto lets it choose based on environment. | auto |
| channels.discord.enabled | Enables or disables the Discord channel integration. | |
| channels.discord.groupPolicy | Controls which Discord groups or channels are allowed to talk to the agent. | allowlist |
Troubleshooting
Fly deploy shows “App is not listening on expected address”.
0, so Fly’s proxy can’t reach it. toml so the app listens on the lan interface.
app = "my-openclaw" # Your app name
[processes]
app = "node dist/index.js gateway --allow-unconfigured --port 3000 --bind lan"Health checks failing / connection refused on your Fly app.
Fly can’t reach the gateway on the port it expects, usually because the internal_port and gateway port don’t match. Ensure `internal_port` in `[http_service]` matches `--port 3000` or `OPENCLAW_GATEWAY_PORT=3000` and redeploy.
[http_service]
internal_port = 3000Container keeps restarting with OOM errors like SIGABRT or v8::internal::Runtime_AllocateInYoungGeneration.
The machine doesn’t have enough memory; 512MB is too small and even 1GB can OOM under load. toml or update the machine with a higher vm-memory setting.
[[vm]]
memory = "2048mb"
fly machine update <machine-id> --vm-memory 2048 -yGateway refuses to start with “already running” lock errors after a restart.
A stale PID lock file on the volume makes the gateway think another instance is running. Remove the lock file on /data and restart the machine so it can start cleanly.
fly ssh console --command "rm -f /data/gateway.*.lock"
fly machine restart <machine-id>Config changes don’t seem to apply and the gateway behaves like it has no config.
json. mode="local"` for a normal local gateway start.
fly ssh console --command "cat /data/openclaw.json"You can’t write openclaw.json using fly ssh console -C with shell redirection.
The -C mode doesn’t support shell redirection, so the file never gets created. Pipe your config via tee or use fly sftp, deleting any existing file first if sftp complains.
# Use echo + tee (pipe from local to remote)
echo '{"your":"config"}' | fly ssh console -C "tee /data/openclaw.json"
# Or use sftp
fly sftp shell
> put /local/path/config.json /data/openclaw.json
# Delete existing file if needed
fly ssh console --command "rm /data/openclaw.json"Auth profiles, channel state, or sessions disappear after a restart.
State is being written to the container filesystem instead of the mounted volume. toml and redeploy so OpenClaw writes to the persistent volume.
[env]
OPENCLAW_STATE_DIR = "/data"Frequently asked questions
Powered by Mem0
Add persistent memory to OpenClaw
Official Mem0 plugin for OpenClaw keeps context across chats and tools. Smaller prompts, lower cost, better continuity for your agents.