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.
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.
| Fly.io Component | AWS Equivalent (via Encore) |
|---|---|
| Fly Machines | Fargate |
| Fly Postgres | Amazon RDS |
| Fly Volumes | EBS or S3 |
| Fly.io Regions | AWS 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.
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.
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.
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:
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");
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.
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.
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.
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);
}
);
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
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.
If your Fly app runs background jobs (separate process or Machines), migrate them to Encore's Pub/Sub or 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,
});
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);
},
});
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.
Push your code:
git push encore main
Run data migrations after the first deploy (database import, file sync)
Update DNS to point to your new endpoints
Scale down Fly Machines once you've verified everything works
Encore creates these resources in your AWS account:
You can view and manage these resources directly in the AWS console.
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.
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.