Fly.io Deployment
Goal: Moltbot Gateway running on a Fly.io machine with persistent storage, automatic HTTPS, and Discord/channel access.What you need
- flyctl CLI installed
- Fly.io account (free tier works)
- Model auth: Anthropic API key (or other provider keys)
- 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 Control UI
1) Create the Fly app
lhr (London), iad (Virginia), sjc (San Jose).
2) Configure fly.toml
Editfly.toml to match your app name and requirements.
Security note: The default config exposes a public URL. For a hardened deployment with no public IP, see Private Deployment or use fly.private.toml.
| 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’ll create one after) |
internal_port = 3000 | Must match --port 3000 (or CLAWDBOT_GATEWAY_PORT) for Fly health checks |
memory = "2048mb" | 512MB is too small; 2GB recommended |
CLAWDBOT_STATE_DIR = "/data" | Persists state on the volume |
3) Set secrets
- Non-loopback binds (
--bind lan) requireCLAWDBOT_GATEWAY_TOKENfor security. - Treat these tokens like passwords.
- Prefer env vars over config file for all API keys and tokens. This keeps secrets out of
moltbot.jsonwhere they could be accidentally exposed or logged.
4) Deploy
5) Create config file
SSH into the machine to create a proper config:CLAWDBOT_STATE_DIR=/data, the config path is /data/moltbot.json.
Note: The Discord token can come from either:
- Environment variable:
DISCORD_BOT_TOKEN(recommended for secrets) - Config file:
channels.discord.token
DISCORD_BOT_TOKEN automatically.
Restart to apply:
6) Access the Gateway
Control UI
Open in browser:https://my-moltbot.fly.dev/
Paste your gateway token (the one from CLAWDBOT_GATEWAY_TOKEN) to authenticate.
Logs
SSH Console
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 can’t reach the gateway on the configured port. Fix: Ensureinternal_port matches the gateway port (set --port 3000 or CLAWDBOT_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. This happens when the container restarts but the PID lock file persists on the volume. Fix: Delete the lock file:/data/gateway.*.lock (not in a subdirectory).
Config Not Being Read
If using--allow-unconfigured, the gateway creates a minimal config. Your custom config at /data/moltbot.json should be read on restart.
Verify the config exists:
Writing Config via SSH
Thefly ssh console -C command doesn’t 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 credentials or sessions after a restart, the state dir is writing to the container filesystem. Fix: EnsureCLAWDBOT_STATE_DIR=/data is set in fly.toml and redeploy.
Updates
Updating Machine Command
If you need to change the startup command without a full redeploy:fly deploy, the machine command may reset to what’s in fly.toml. If you made manual changes, re-apply them after deploy.
Private Deployment (Hardened)
By default, Fly allocates public IPs, making your gateway accessible athttps://your-app.fly.dev. This is convenient but means your deployment is discoverable by internet scanners (Shodan, Censys, etc.).
For a hardened deployment with no public exposure, use the private template.
When to use private deployment
- You only make outbound calls/messages (no inbound webhooks)
- You use ngrok or Tailscale tunnels for any webhook callbacks
- You access the gateway via SSH, proxy, or WireGuard instead of browser
- You want the deployment hidden from internet scanners
Setup
Usefly.private.toml instead of the standard config:
fly ips list should show only a private type IP:
Accessing a private deployment
Since there’s no public URL, use one of these methods: Option 1: Local proxy (simplest)Webhooks with private deployment
If you need 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 fine for outbound calls without webhooks
Security benefits
| 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 (not ARM)
- The Dockerfile is compatible with both architectures
- For WhatsApp/Telegram onboarding, use
fly ssh console - Persistent data lives on the volume at
/data - Signal requires Java + signal-cli; use a custom image and keep memory at 2GB+.
Cost
With the recommended config (shared-cpu-2x, 2GB RAM):
- ~$10-15/month depending on usage
- Free tier includes some allowance