Platform setup

How to Deploy OpenClaw on Fly.io

4 min read

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.

Setup flow

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. 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. 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.

    toml
    app = "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. 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. 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.

    bash
    fly deploy
    
    fly status
    fly logs
    
    [gateway] listening on ws://0.0.0.0:3000 (PID xxx)
    [discord] logged in to discord as xxx
  5. 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.

    bash
    fly 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. 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.

    bash
    fly open
    
    fly logs              # Live logs
    fly logs --no-tail    # Recent logs
    
    fly ssh console
  7. 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. 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

OptionDescriptionExample
NODE_ENVSets the Node.js environment mode for the OpenClaw process.production
OPENCLAW_PREFER_PNPMTells OpenClaw to prefer pnpm for package management inside the container.1
OPENCLAW_STATE_DIRDirectory where OpenClaw stores state and config so it persists on the Fly volume./data
NODE_OPTIONSConfigures Node.js runtime options such as the maximum old-space heap size.--max-old-space-size=1536
OPENCLAW_GATEWAY_TOKENShared secret token required when the gateway binds to a non-loopback address.7f3c2a9e4b1d8c6f0e2a9b3c5d7e1f09c3a4b6d8e0f1a2c3d4e5f6a7b8c9d0e
ANTHROPIC_API_KEYAnthropic API key used for Claude model calls in your OpenClaw agents.sk-ant-abc1234567890
OPENAI_API_KEYOpenAI API key used for OpenAI-backed models configured in your agents.sk-abc1234567890
GOOGLE_API_KEYGoogle API key used when you enable Google-based model providers.AIzaSyA-abc1234567890
DISCORD_BOT_TOKENDiscord bot token that the gateway uses to log in and handle Discord messages.MTQ1Njc4OTAxMjM0NTY3ODkw.XYZ
gateway.modeControls how the gateway runs; local mode starts a normal local gateway.local
gateway.bindBinding mode for the gateway inside openclaw.json; auto lets it choose based on environment.auto
channels.discord.enabledEnables or disables the Discord channel integration.
channels.discord.groupPolicyControls 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.

bash
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.

bash
[http_service]
  internal_port = 3000

Container 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.

bash
[[vm]]
  memory = "2048mb"

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

Gateway 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.

bash
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.

bash
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.

bash
# 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.

bash
[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.

More in Platform setup