Motia Icon
Deployment Guide

Deploy to Fly.io

Deploy your Motia app to Fly.io with Upstash Redis for global edge deployment

Fly.io runs your app on fast micro-VMs close to your users. Combined with Upstash Redis, you get a globally distributed Motia backend.

This guide walks you through deploying a Motia app to Fly.io with production Redis.

What you'll get: A containerized Motia app running on Fly.io with Upstash Redis for state, events, streams, and cron locking.

Example Project: Follow along with the Todo App example - a complete deployment-ready Motia app with Redis configuration.


Prerequisites

Before you start:

  • A Fly.io account (free tier works)
  • Fly CLI installed
  • Docker running locally (for testing)
  • A Motia project ready to deploy

Install the Fly CLI:

# macOS
brew install flyctl
 
# Windows
powershell -Command "iwr https://fly.io/install.ps1 -useb | iex"
 
# Linux
curl -L https://fly.io/install.sh | sh

Login:

flyctl auth login

Quick Start

Generate Docker files

From your Motia project root:

npx motia@latest docker setup

This creates Dockerfile and .dockerignore.

Create fly.toml

fly.toml
app = "my-motia-app"
primary_region = "sjc"
 
[build]
  dockerfile = "Dockerfile"
 
[http_service]
  internal_port = 3000
  force_https = true
  auto_stop_machines = false
  auto_start_machines = true
  min_machines_running = 1
  processes = ["app"]
 
[[vm]]
  memory = "1gb"
  cpu_kind = "shared"
  cpus = 1

Replace my-motia-app with your app name and sjc with your preferred region.

Launch your app

flyctl launch --no-deploy

This creates your app on Fly without deploying yet.

Add Upstash Redis

flyctl redis create

Follow the prompts to create a Redis instance. Choose a region close to your app.

Upstash Redis on Fly requires a credit card on file, even for free tier usage.

Set environment variables

flyctl secrets set NODE_ENV=production USE_REDIS=true

The Redis URL is automatically attached when you create Redis with flyctl redis create.

Deploy

flyctl deploy

Fly builds your Docker image and deploys it globally.

Get your URL

Your app is live at: https://my-motia-app.fly.dev


Project Setup

Update Your Start Script

Fly injects the port via environment variables. Update your package.json:

package.json
{
  "scripts": {
    "start": "motia start --port ${PORT:-3000} --host 0.0.0.0"
  }
}

The --host 0.0.0.0 is important - Fly needs your app to listen on all interfaces.

Configure Redis

Motia supports two approaches for production Redis configuration:

Use Motia's built-in redis configuration option:

motia.config.ts
import { config } from 'motia'
import statesPlugin from '@motiadev/plugin-states/plugin'
import endpointPlugin from '@motiadev/plugin-endpoint/plugin'
import logsPlugin from '@motiadev/plugin-logs/plugin'
import observabilityPlugin from '@motiadev/plugin-observability/plugin'
import bullmqPlugin from '@motiadev/plugin-bullmq/plugin'
 
// Determine Redis configuration based on environment
const getRedisConfig = () => {
  const useExternalRedis = process.env.USE_REDIS === 'true' || 
    (process.env.USE_REDIS !== 'false' && process.env.NODE_ENV === 'production')
 
  if (!useExternalRedis) {
    return { useMemoryServer: true as const }
  }
 
  const redisUrl = process.env.REDIS_URL
  if (redisUrl) {
    try {
      const url = new URL(redisUrl)
      return {
        useMemoryServer: false as const,
        host: url.hostname,
        port: parseInt(url.port || '6379', 10),
        password: url.password || undefined,
        username: url.username || undefined,
      }
    } catch (e) {
      console.error('[motia] Failed to parse REDIS_URL:', e)
    }
  }
 
  return {
    useMemoryServer: false as const,
    host: process.env.REDIS_HOST || 'localhost',
    port: parseInt(process.env.REDIS_PORT || '6379', 10),
    password: process.env.REDIS_PASSWORD,
    username: process.env.REDIS_USERNAME,
  }
}
 
export default config({
  plugins: [
    observabilityPlugin,
    statesPlugin,
    endpointPlugin,
    logsPlugin,
    bullmqPlugin,
  ],
  redis: getRedisConfig(),
})

That's it! The config parses REDIS_URL automatically. Fly sets this when you run flyctl redis create.

Option 2: Custom Adapters (Advanced)

For more control over individual adapters:

npm install @motiadev/adapter-redis-state \
            @motiadev/adapter-redis-streams \
            @motiadev/adapter-redis-cron \
            @motiadev/adapter-bullmq-events
motia.config.ts
import { config } from 'motia'
import { RedisStateAdapter } from '@motiadev/adapter-redis-state'
import { RedisStreamAdapterManager } from '@motiadev/adapter-redis-streams'
import { RedisCronAdapter } from '@motiadev/adapter-redis-cron'
import { BullMQEventAdapter } from '@motiadev/adapter-bullmq-events'
import statesPlugin from '@motiadev/plugin-states/plugin'
import endpointPlugin from '@motiadev/plugin-endpoint/plugin'
import logsPlugin from '@motiadev/plugin-logs/plugin'
import observabilityPlugin from '@motiadev/plugin-observability/plugin'
 
// Parse REDIS_URL (set automatically by Fly when you attach Upstash Redis)
// Format: rediss://default:password@host:port
const url = new URL(process.env.REDIS_URL || 'redis://localhost:6379')
 
const redisConfig = {
  host: url.hostname,
  port: Number(url.port) || 6379,
  username: url.username || undefined,
  password: url.password || undefined,
  tls: url.protocol === 'rediss:',  // Upstash requires TLS
}
 
const useRedis = process.env.USE_REDIS === 'true' || process.env.NODE_ENV === 'production'
 
export default config({
  plugins: [
    observabilityPlugin,
    statesPlugin,
    endpointPlugin,
    logsPlugin,
  ],
  adapters: useRedis ? {
    state: new RedisStateAdapter({
      socket: { host: redisConfig.host, port: redisConfig.port, tls: redisConfig.tls },
      username: redisConfig.username,
      password: redisConfig.password,
    }),
    streams: new RedisStreamAdapterManager({
      socket: { host: redisConfig.host, port: redisConfig.port, tls: redisConfig.tls },
      username: redisConfig.username,
      password: redisConfig.password,
    }),
    events: new BullMQEventAdapter({
      connection: {
        host: redisConfig.host,
        port: redisConfig.port,
        username: redisConfig.username,
        password: redisConfig.password,
        tls: redisConfig.tls ? {} : undefined,
        maxRetriesPerRequest: null,
      },
    }),
    cron: new RedisCronAdapter({
      socket: { host: redisConfig.host, port: redisConfig.port, tls: redisConfig.tls },
      username: redisConfig.username,
      password: redisConfig.password,
    }),
  } : undefined,
})

Fly.io vs Railway

FeatureFly.ioRailway
Global regions30+ regionsLimited regions
RedisUpstash (external)Built-in Redis
PricingPay-per-useUsage-based
CLIflyctlrailway
Best forEdge deploymentSimple setup

Choose Fly.io if you need low-latency responses globally. Choose Railway for simpler Redis setup.


Common Commands

# Check app status
flyctl status
 
# View logs (streams in real-time)
flyctl logs
 
# SSH into your app
flyctl ssh console
 
# Scale machines
flyctl scale count 3
 
# List secrets
flyctl secrets list
 
# Destroy app
flyctl apps destroy my-motia-app

Troubleshooting

App Not Listening on Expected Address

Symptom: Fly warns "app is not listening on the expected address"

Fix: Make sure your start script includes --host 0.0.0.0:

"start": "motia start --port ${PORT:-3000} --host 0.0.0.0"

Redis Connection Refused

Symptom: Logs show ECONNREFUSED 127.0.0.1:6379

Cause: App is trying to connect to local Redis instead of Upstash.

Fix:

  1. Check if REDIS_URL is set: flyctl secrets list
  2. Create Redis if missing: flyctl redis create
  3. Verify your config parses the URL correctly

TLS Connection Errors

Symptom: Redis connection fails with TLS/SSL errors

Cause: Upstash requires TLS (rediss://), but your config isn't enabling it.

Fix: Make sure your config enables TLS when the protocol is rediss://:

const useTls = url.protocol === 'rediss:'

Plugin Not Loading

Cause: Plugin imports might not be resolving correctly.

Fix: Use ESM imports (recommended for "type": "module" projects):

// ✅ Correct - ESM imports
import statesPlugin from '@motiadev/plugin-states/plugin'
import endpointPlugin from '@motiadev/plugin-endpoint/plugin'

Machine Keeps Restarting

Symptom: App restarts repeatedly in logs

Common causes:

  1. Unhandled exceptions during startup
  2. Missing environment variables
  3. Port binding issues

Debug: Check logs for the actual error:

flyctl logs

Scaling Globally

Fly makes global deployment easy. Add machines in different regions:

# Add a machine in Amsterdam
flyctl machine clone --region ams
 
# Add a machine in Sydney  
flyctl machine clone --region syd

With Redis configured, all machines share state automatically. Users connect to the nearest machine for lowest latency.


What's Next?

Need help? See our Community Resources for questions, examples, and discussions.
Deploy to Fly.io | motia