01/22/26

How to Migrate from Fly.io to AWS

Move from Fly.io to your own AWS account for more control

9 Min Read

Fly.io is good at one thing: running containers close to users. Their global deployment model spins up Machines in regions around the world, giving you low latency without thinking about infrastructure. But that convenience comes with tradeoffs.

You can't access the underlying infrastructure. You can't connect to existing AWS resources without networking workarounds. Pricing can be unpredictable at scale. And if you need compliance controls or infrastructure auditing, Fly.io doesn't offer that.

The traditional path to AWS means learning Terraform or CloudFormation, writing Dockerfiles, and managing infrastructure config. That's a big jump from Fly.io's fly deploy.

This guide takes a different approach: migrating to your own AWS account using Encore and Encore Cloud. Encore is an open-source TypeScript backend framework (11k+ GitHub stars) where you define infrastructure as type-safe objects in your code: databases, Pub/Sub, cron jobs, object storage. Encore Cloud then provisions these resources in your AWS account using managed services like RDS, SQS, and S3.

Infrastructure from Code: define resources in TypeScript, deploy to AWS or GCP

The result is AWS infrastructure you own and control, but with a simple deployment experience: push code, get a deployment. You trade Fly.io's global edge for infrastructure ownership and AWS ecosystem access. Companies like Groupon already use this approach to power their backends at scale.

What You're Migrating

Fly.io ComponentAWS Equivalent (via Encore)
Fly MachinesFargate
Fly PostgresAmazon RDS
Fly VolumesEBS or S3
Fly.io RegionsAWS Regions

The mapping is mostly direct. Your application code runs on Fargate. Postgres moves to RDS. Volumes become S3 for object storage or EBS for block storage.

Why Teams Migrate from Fly.io

Infrastructure ownership: Fly.io manages everything behind the scenes. You can't access the VPC, configure security groups, or see what's actually running. With AWS, you own the infrastructure and can configure it however you need.

AWS ecosystem access: If you need SQS, DynamoDB, ElastiCache, or other AWS services, using them from Fly.io requires networking configuration and adds latency. In your own AWS account, these services are local.

Compliance requirements: Healthcare, finance, and government projects often require infrastructure in accounts with specific audit capabilities. Fly.io's shared infrastructure doesn't meet these requirements.

Cost predictability: Fly.io's pricing scales with usage, which can be hard to forecast. AWS reserved capacity and savings plans provide predictable costs.

What Encore Handles For You

When you deploy to AWS through Encore Cloud, every resource gets production defaults: private VPC placement, least-privilege IAM roles, encryption at rest, automated backups where applicable, and CloudWatch logging. You don't configure this per resource. It's automatic.

Encore follows AWS best practices and gives you guardrails. You can review infrastructure changes before they're applied, and everything runs in your own AWS account so you maintain full control.

Here's what that looks like in practice:

import { SQLDatabase } from "encore.dev/storage/sqldb"; import { Bucket } from "encore.dev/storage/objects"; import { Topic } from "encore.dev/pubsub"; import { CronJob } from "encore.dev/cron"; const db = new SQLDatabase("main", { migrations: "./migrations" }); const uploads = new Bucket("uploads", { versioned: false }); const events = new Topic<OrderEvent>("events", { deliveryGuarantee: "at-least-once" }); const _ = new CronJob("daily-cleanup", { schedule: "0 0 * * *", endpoint: cleanup });

This provisions RDS, S3, SNS/SQS, and CloudWatch Events with proper networking, IAM, and monitoring. You write TypeScript or Go, Encore handles the Terraform. The only Encore-specific parts are the import statements. Your business logic is standard TypeScript, so you're not locked in.

See the infrastructure primitives docs for the full list of supported resources.

The Global Edge Tradeoff

Fly.io's main advantage is automatic global deployment. Your app runs in 30+ regions, close to users everywhere. AWS via Encore deploys to single regions by default.

For many applications, this doesn't matter. A single AWS region with CloudFront CDN handles global traffic well. Latency-sensitive applications (real-time games, video conferencing) might need more thought.

If you currently use Fly.io's multi-region feature and actually need it, plan for how you'll handle this on AWS. Options include:

  • Single region + CDN: Works for most web applications
  • Multiple Encore environments: Deploy to different AWS regions and route with Route 53
  • CloudFront for static assets: Cache static content at edge locations

Step 1: Migrate Your Application

Fly.io applications use Dockerfiles or buildpacks. With Encore, you write application code and containerization is handled automatically.

fly.toml:

app = "my-api" primary_region = "sjc" [http_service] internal_port = 8080 force_https = true auto_stop_machines = true auto_start_machines = true [env] NODE_ENV = "production"

Encore equivalent:

import { api } from "encore.dev/api"; export const health = api( { method: "GET", path: "/health", expose: true }, async () => ({ status: "ok" }) ); export const hello = api( { method: "GET", path: "/hello/:name", expose: true }, async ({ name }: { name: string }) => { return { message: `Hello, ${name}!` }; } );

No configuration files needed. Encore analyzes your code to understand what infrastructure it needs. When you deploy to AWS, it provisions Fargate tasks with appropriate networking, load balancing, and auto-scaling.

If your Fly app has multiple processes (web + worker), split them into Encore services:

// api/encore.service.ts import { Service } from "encore.dev/service"; export default new Service("api"); // worker/encore.service.ts import { Service } from "encore.dev/service"; export default new Service("worker");

Step 2: Migrate Fly Postgres to RDS

Export from Fly Postgres

Connect to your Fly Postgres and export:

# Start a proxy to your Fly Postgres fly proxy 5432 -a my-db & # Export with pg_dump pg_dump "postgres://postgres:password@localhost:5432/mydb" > backup.sql

Alternatively, use the Fly Postgres connection directly if your network allows it.

Set Up the Encore Database

Define your database in code:

import { SQLDatabase } from "encore.dev/storage/sqldb"; const db = new SQLDatabase("main", { migrations: "./migrations", });

That's the complete database definition. Encore analyzes this at compile time and provisions RDS PostgreSQL when you deploy.

Put your existing migrations in the ./migrations directory, or create new ones matching your current schema.

Import to RDS

After your first Encore deploy, get the RDS connection string and import:

# Get the production RDS connection encore db conn-uri main --env=production # Import your data psql "postgresql://user:pass@your-rds.amazonaws.com:5432/main" < backup.sql

For large databases, consider using AWS Database Migration Service (DMS) for a live migration with minimal downtime.

Step 3: Replace Fly Volumes with S3

Fly Volumes are persistent disks attached to Machines. They're useful for storing files locally, but they're tied to specific Machines and don't scale well.

S3 is a better fit for object storage on AWS. Files are accessible from any service, automatically replicated, and essentially unlimited in size.

Before (Fly with volumes):

import fs from "fs"; import path from "path"; // Write to mounted volume const uploadPath = path.join("/data/uploads", filename); fs.writeFileSync(uploadPath, buffer); // Read from volume const file = fs.readFileSync(uploadPath);

After (Encore with S3):

import { Bucket } from "encore.dev/storage/objects"; const uploads = new Bucket("uploads", { versioned: false }); export const uploadFile = api( { method: "POST", path: "/upload", expose: true }, async (req: { filename: string; data: Buffer; contentType: string }) => { await uploads.upload(req.filename, req.data, { contentType: req.contentType, }); return { url: uploads.publicUrl(req.filename) }; } ); export const downloadFile = api( { method: "GET", path: "/files/:filename", expose: true }, async ({ filename }: { filename: string }) => { return await uploads.download(filename); } );

Migrate Existing Files

If you have files on Fly Volumes, you'll need to move them:

# SSH into your Fly Machine fly ssh console -a my-app # Compress the data directory tar -czvf /tmp/uploads.tar.gz /data/uploads # Download locally (from another terminal) fly sftp get /tmp/uploads.tar.gz # Extract and upload to S3 after Encore deployment tar -xzvf uploads.tar.gz aws s3 sync ./uploads s3://your-encore-bucket

Step 4: Migrate Secrets

Fly.io secrets become Encore secrets:

Fly.io:

fly secrets set DATABASE_URL="postgres://..." fly secrets set STRIPE_KEY="sk_live_..."

Encore:

encore secret set --type=production DatabaseURL encore secret set --type=production StripeKey

Use them in code:

import { secret } from "encore.dev/config"; const stripeKey = secret("StripeKey"); // Use the secret const stripe = new Stripe(stripeKey());

Encore secrets are environment-specific (development, staging, production) and encrypted at rest.

Step 5: Handle Background Processing

If your Fly app runs background jobs (separate process or Machines), migrate them to Encore's Pub/Sub or cron:

For Scheduled Jobs: Use Cron

import { CronJob } from "encore.dev/cron"; import { api } from "encore.dev/api"; export const dailyReport = api( { method: "POST", path: "/internal/daily-report" }, async () => { await generateAndSendDailyReport(); return { success: true }; } ); const _ = new CronJob("daily-report", { title: "Generate daily report", schedule: "0 8 * * *", // 8 AM UTC endpoint: dailyReport, });

For Job Queues: Use Pub/Sub

import { Topic, Subscription } from "encore.dev/pubsub"; interface ImageProcessingJob { imageId: string; operation: "resize" | "compress" | "watermark"; } const imageJobs = new Topic<ImageProcessingJob>("image-jobs", { deliveryGuarantee: "at-least-once", }); // Enqueue a job export const processImage = api( { method: "POST", path: "/images/:id/process", expose: true }, async ({ id }: { id: string }) => { await imageJobs.publish({ imageId: id, operation: "resize" }); return { queued: true }; } ); // Process jobs const _ = new Subscription(imageJobs, "image-processor", { handler: async (job) => { await processImageJob(job.imageId, job.operation); }, });

Step 6: Deploy to AWS

  1. Connect your AWS account in the Encore Cloud dashboard. You'll set up an IAM role that gives Encore permission to provision resources. See the AWS setup guide for details.

  2. Push your code:

    git push encore main
  3. Run data migrations after the first deploy (database import, file sync)

  4. Update DNS to point to your new endpoints

  5. Scale down Fly Machines once you've verified everything works

What Gets Provisioned

Encore creates these resources in your AWS account:

  • Fargate for running your application
  • RDS PostgreSQL for your database
  • S3 for object storage
  • Application Load Balancer for HTTP routing
  • CloudWatch for logs and metrics
  • IAM roles with least-privilege access

You can view and manage these resources directly in the AWS console.

Handling Multi-Region (if needed)

If you relied on Fly.io's global deployment:

Option 1: Single region is often enough Most web applications work fine from a single region. Add CloudFront for static asset caching to reduce latency for global users.

Option 2: Multiple Encore environments Create separate environments in different AWS regions (us-east-1, eu-west-1, ap-southeast-1) and use Route 53 geolocation routing to direct users to the nearest region.

Option 3: Compute at edge, data centralized Use Lambda@Edge or CloudFront Functions for latency-sensitive operations while keeping your database and core logic in one region.

Migration Checklist

  • Audit Fly.io app structure and dependencies
  • Export Postgres database with pg_dump
  • Create Encore app with database migrations
  • Convert Fly processes to Encore services
  • Migrate files from Volumes to S3
  • Move secrets to Encore secrets
  • Convert background jobs to Pub/Sub or cron
  • Test in Encore preview environment
  • Deploy to production
  • Run data migrations
  • Update DNS
  • Monitor for issues
  • Scale down Fly Machines

Wrapping Up

Migrating from Fly.io to AWS trades automatic global distribution for infrastructure ownership and AWS ecosystem access. For most applications, the tradeoff is worth it. You get predictable costs, compliance capabilities, and direct access to AWS services.

If you genuinely need global edge deployment, evaluate whether CloudFront + single region works for your use case, or plan for a multi-region Encore setup.

Ready to escape the maze of complexity?

Encore Cloud is the development platform for building robust type-safe distributed systems with declarative infrastructure.