Fly.io Deployment
Goal: FluffBuzz 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: 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 Control UI
1
Create the Fly app
lhr (London), iad (Virginia), sjc (San Jose).2
Configure fly.toml
Edit Key settings:
fly.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.3
Set secrets
- Non-loopback binds (
--bind lan) require a valid gateway auth path. This Fly.io example usesFLUFFBUZZ_GATEWAY_TOKEN, butgateway.auth.passwordor a correctly configured non-loopbacktrusted-proxydeployment also satisfy the requirement. - Treat these tokens like passwords.
- Prefer env vars over config file for all API keys and tokens. This keeps secrets out of
fluffbuzz.jsonwhere they could be accidentally exposed or logged.
4
Deploy
5
Create config file
SSH into the machine to create a proper config:Create the config directory and file:Note: With
FLUFFBUZZ_STATE_DIR=/data, the config path is /data/fluffbuzz.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: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 FLUFFBUZZ_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
--allow-unconfigured only bypasses the startup guard. It does not create or repair /data/fluffbuzz.json, so make sure your real config exists and includes gateway.mode="local" when you want a normal local gateway start.
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 auth profiles, channel/provider state, or sessions after a restart, the state dir is writing to the container filesystem. Fix: EnsureFLUFFBUZZ_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
webhookSecurity.allowedHosts to the public tunnel hostname so forwarded host headers are accepted.
Security benefits
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
Next steps
- Set up messaging channels: Channels
- Configure the Gateway: Gateway configuration
- Keep FluffBuzz up to date: Updating