Skip to main content

Deploy iCommerce on Fly.io

Deploy StateSet iCommerce on Fly.io with automatic HTTPS, persistent volumes, and global edge deployment. This guide walks you through deploying a production-ready iCommerce Gateway that scales automatically.

Goal

Deploy a StateSet iCommerce Gateway on Fly.io with:
  • Persistent storage for configuration and workspace data
  • Automatic HTTPS with custom domain support
  • Discord, Telegram, and other channel integrations
  • Global edge deployment for low latency
Fly.io’s free tier works for testing. Production deployments typically cost $10-15/month with the recommended configuration.

What you’ll build

1

Create Fly app and volume

Set up the application and persistent storage.
2

Configure fly.toml

Define build settings, environment, and resources.
3

Set secrets

Configure API keys and tokens securely.
4

Deploy

Build and deploy the Gateway.
5

Configure the Gateway

Create the configuration file and connect channels.

Prerequisites

Before you begin, ensure you have:
  • flyctl CLI installed
  • Fly.io account (free tier works)
  • StateSet API credentials
  • Model provider credentials (Anthropic, OpenAI, etc.)
Optional integrations:
  • Discord bot token
  • Telegram bot token
  • WhatsApp Business API credentials

Quick path (experienced operators)

If you’re familiar with Fly.io, follow this condensed workflow:
  1. Clone repo and customize fly.toml
  2. Create app and volume
  3. Set secrets via fly secrets set
  4. Deploy with fly deploy
  5. SSH in to create config or use Control UI

1) Create the Fly app

Clone the repository and create a new Fly app:
git clone https://github.com/stateset/stateset-icommerce.git
cd stateset-icommerce

# Create a new Fly app (choose your own name)
fly apps create my-icommerce

# Create a persistent volume (1GB is usually enough)
fly volumes create icommerce_data --size 1 --region iad
Choose a region close to you or your users. Common options: lhr (London), iad (Virginia), sjc (San Jose), fra (Frankfurt).

2) Configure fly.toml

Create or edit fly.toml to match your app name and requirements:
app = "my-icommerce"  # Your app name
primary_region = "iad"

[build]
  dockerfile = "Dockerfile"

[env]
  NODE_ENV = "production"
  STATESET_PREFER_PNPM = "1"
  STATESET_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 = "icommerce_data"
  destination = "/data"

Key settings explained

SettingPurpose
--bind lanBinds to 0.0.0.0 so Fly’s proxy can reach the gateway
--allow-unconfiguredStarts without a config file (create one after deploy)
internal_port = 3000Must match --port 3000 for Fly health checks
memory = "2048mb"512MB is too small; 2GB recommended
STATESET_STATE_DIR = "/data"Persists state on the volume
The default config exposes a public URL. For a hardened deployment with no public IP, see the Private Deployment section.

3) Set secrets

Configure your API keys and tokens as Fly secrets:
# Required: Gateway token (for non-loopback binding)
fly secrets set STATESET_GATEWAY_TOKEN=$(openssl rand -hex 32)

# Model provider API keys
fly secrets set ANTHROPIC_API_KEY=sk-ant-...
fly secrets set OPENAI_API_KEY=sk-...

# Optional: Other providers
fly secrets set GOOGLE_API_KEY=...

# Channel tokens
fly secrets set DISCORD_BOT_TOKEN=MTQ...
fly secrets set TELEGRAM_BOT_TOKEN=...
Non-loopback binds (--bind lan) require STATESET_GATEWAY_TOKEN for security. Treat these tokens like passwords.
Prefer environment variables over config files for all API keys and tokens. This keeps secrets out of configuration files where they could be accidentally exposed or logged.

4) Deploy

Deploy the application:
fly deploy
The first deploy builds the Docker image (~2-3 minutes). Subsequent deploys are faster. Verify the deployment:
fly status
fly logs
Success output:
[gateway] listening on ws://0.0.0.0:3000 (PID xxx)
[discord] logged in to discord as xxx

5) Create configuration file

SSH into the machine to create a proper configuration:
fly ssh console
Create the config directory and file:
mkdir -p /data
cat > /data/stateset.json << 'EOF'
{
  "agents": {
    "defaults": {
      "model": {
        "primary": "anthropic/claude-sonnet-4-20250514",
        "fallbacks": ["openai/gpt-4o"]
      },
      "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"
  }
}
EOF
With STATESET_STATE_DIR=/data, the config path is /data/stateset.json.
Restart to apply the configuration:
exit
fly machine restart <machine-id>

6) Access the Gateway

Control UI

Open in your browser:
fly open
Or visit https://my-icommerce.fly.dev/ Enter your gateway token (from STATESET_GATEWAY_TOKEN) to authenticate.

Logs

fly logs              # Live logs
fly logs --no-tail    # Recent logs

SSH Console

fly ssh console

Troubleshooting

”App is not listening on expected address”

The gateway is binding to 127.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: Ensure internal_port matches the gateway port (set --port 3000 or STATESET_GATEWAY_PORT=3000).

OOM / Memory issues

Container keeps restarting or getting killed. Signs: SIGABRT, memory allocation errors, or silent restarts. Fix: Increase memory in fly.toml:
[[vm]]
  memory = "2048mb"
Or update an existing machine:
fly machine update <machine-id> --vm-memory 2048 -y
512MB is too small. 1GB may work but can OOM under load. 2GB is recommended.

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:
fly ssh console --command "rm -f /data/gateway.*.lock"
fly machine restart <machine-id>

Config not being read

If using --allow-unconfigured, the gateway creates a minimal config. Your custom config at /data/stateset.json should be read on restart. Verify the config exists:
fly ssh console --command "cat /data/stateset.json"

Writing config via SSH

The fly ssh console -C command doesn’t support shell redirection. To write a config file:
# Use echo + tee (pipe from local to remote)
echo '{"your":"config"}' | fly ssh console -C "tee /data/stateset.json"

# Or use sftp
fly sftp shell
> put /local/path/config.json /data/stateset.json
fly sftp may fail if the file already exists. Delete first with fly ssh console --command "rm /data/stateset.json".

State not persisting

If you lose credentials or sessions after a restart, the state directory is writing to the container filesystem. Fix: Ensure STATESET_STATE_DIR=/data is set in fly.toml and redeploy.

Updates

# Pull latest changes
git pull

# Redeploy
fly deploy

# Check health
fly status
fly logs

Updating machine command

If you need to change the startup command without a full redeploy:
# 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
After 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 at https://your-app.fly.dev. This is convenient but means your deployment is discoverable by internet scanners.

When to use private deployment

  • You only make outbound calls/messages (no inbound webhooks)
  • You use ngrok or Tailscale tunnels for webhook callbacks
  • You access the gateway via SSH, proxy, or WireGuard
  • You want the deployment hidden from internet scanners

Setup

Use fly.private.toml instead of the standard config:
fly deploy -c fly.private.toml
Or convert an existing deployment:
# List current IPs
fly ips list -a my-icommerce

# Release public IPs
fly ips release <public-ipv4> -a my-icommerce
fly ips release <public-ipv6> -a my-icommerce

# Deploy with private config
fly deploy -c fly.private.toml

# Allocate private-only IPv6
fly ips allocate-v6 --private -a my-icommerce
After this, fly ips list should show only a private type IP:
VERSION  IP                   TYPE             REGION
v6       fdaa:x:x:x:x::x      private          global

Accessing a private deployment

# Forward local port 3000 to the app
fly proxy 3000:3000 -a my-icommerce

# Then open http://localhost:3000 in browser

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 work fine for outbound calls without webhooks

Security comparison

AspectPublicPrivate
Internet scannersDiscoverableHidden
Direct attacksPossibleBlocked
Control UI accessBrowserProxy/VPN
Webhook deliveryDirectVia tunnel

Cost

With the recommended configuration (shared-cpu-2x, 2GB RAM):
ComponentCost
Compute~$10-12/month
Storage (1GB)~$0.15/month
BandwidthVaries by usage
Total~$10-15/month
Fly.io’s free tier includes some allowance. See Fly.io pricing for details.

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

Next steps