Oct 28, 20266 min read

How to Move a Go API from Fly.io to AWS

Moving a Go app, Fly Postgres, Fly secrets and a scheduled Machine to your own AWS account.

Fly.io suits Go well: a small static binary, a fly.toml with an http_service block, and Machines that start close to users in several regions. Teams start to look elsewhere when the Postgres cluster needs more care than they want to give it, when the data has to live in an AWS account the company controls, or when the rest of the product already runs on AWS and the Go API is the one service outside it.

This guide moves a Go orders API off Fly.io. The app has one process group, a Fly Postgres database attached through DATABASE_URL, two secrets and a scheduled Machine that cancels stale orders every hour. It covers the mapping, a tested Encore version of the service, the manual AWS path, the data move and the DNS switch.

Fly.io to AWS mapping at a glance

Fly.ioEncore on AWSManual AWS
App and http_serviceEncore service and API endpointsECS service, task definition, load balancer
Fly Postgressqldb.NewDatabase, provisioned as RDSRDS instance, subnet group, security groups
fly secretsEncore secrets in Secrets ManagerSecrets Manager plus task definition entries
Scheduled Machinecron.NewJob calling an endpointEventBridge Scheduler and an ECS task
VolumesObject storage bucket or databaseS3, or EBS and EFS for disk workloads
RegionsRegion chosen per environmentRegion per stack

Encore: the Fly pieces as declarations in the Go code

Encore combines an open source infrastructure SDK for Go and TypeScript with a platform that deploys into your own AWS or GCP account. The Go handlers move behind Encore endpoints, the database replaces the attached Postgres app, and the hourly job replaces the scheduled Machine:

// Service orders was the web process on the old platform. package orders import ( "context" "errors" "time" "encore.dev/beta/errs" "encore.dev/cron" "encore.dev/storage/sqldb" ) // Replaces the DATABASE_URL add-on: RDS on AWS, local Postgres under `encore run`. var db = sqldb.NewDatabase("orders", sqldb.DatabaseConfig{ Migrations: "./migrations", }) // Replaces the scheduled cleanup task on the old platform. var _ = cron.NewJob("expire-pending", cron.JobConfig{ Title: "Cancel orders left pending for a day", Every: 1 * cron.Hour, Endpoint: ExpirePending, }) type Order struct { ID int64 `json:"id"` Customer string `json:"customer"` TotalCents int64 `json:"totalCents"` Status string `json:"status"` CreatedAt time.Time `json:"createdAt"` } type CreateParams struct { Customer string `json:"customer"` TotalCents int64 `json:"totalCents"` } //encore:api public method=POST path=/orders func Create(ctx context.Context, p *CreateParams) (*Order, error) { if p.Customer == "" || p.TotalCents <= 0 { return nil, &errs.Error{Code: errs.InvalidArgument, Message: "customer and totalCents are required"} } o := &Order{} err := db.QueryRow(ctx, ` INSERT INTO orders (customer, total_cents) VALUES ($1, $2) RETURNING id, customer, total_cents, status, created_at`, p.Customer, p.TotalCents, ).Scan(&o.ID, &o.Customer, &o.TotalCents, &o.Status, &o.CreatedAt) return o, err } //encore:api public method=GET path=/orders/:id func Get(ctx context.Context, id int64) (*Order, error) { o := &Order{} err := db.QueryRow(ctx, ` SELECT id, customer, total_cents, status, created_at FROM orders WHERE id = $1`, id, ).Scan(&o.ID, &o.Customer, &o.TotalCents, &o.Status, &o.CreatedAt) if errors.Is(err, sqldb.ErrNoRows) { return nil, &errs.Error{Code: errs.NotFound, Message: "order not found"} } return o, err } type ExpireResult struct { Cancelled int64 `json:"cancelled"` } // ExpirePending is safe to run twice: it only touches rows still pending. // //encore:api private method=POST path=/orders/expire-pending func ExpirePending(ctx context.Context) (*ExpireResult, error) { res, err := db.Exec(ctx, ` UPDATE orders SET status = 'cancelled' WHERE status = 'pending' AND created_at < now() - interval '24 hours'`) if err != nil { return nil, err } return &ExpireResult{Cancelled: res.RowsAffected()}, nil }

The migration in orders/migrations/1_create_orders.up.sql recreates the table as it exists on Fly:

CREATE TABLE orders ( id BIGSERIAL PRIMARY KEY, customer TEXT NOT NULL, total_cents BIGINT NOT NULL, status TEXT NOT NULL DEFAULT 'pending', created_at TIMESTAMPTZ NOT NULL DEFAULT now() );

Each value from fly secrets list becomes a field on a var secrets struct in the service that reads it, as described in the Go secrets docs, and gets set per environment with encore secret set. Plain [env] values from fly.toml can become constants. Run it locally, where encore run starts Postgres and applies the migration:

~/orders
$ encore run$ curl -X POST localhost:4000/orders -d '{"customer":"initech","totalCents":1200}'{"id":3,"customer":"initech","totalCents":1200,"status":"pending","createdAt":"2026-10-28T12:51:11Z"}$ curl localhost:4000/orders/1{"id":1,"customer":"acme","totalCents":1999,"status":"paid","createdAt":"2026-10-25T11:51:10Z"}$ curl -X POST localhost:4000/orders/expire-pending{"cancelled":1}

On deploy, Encore provisions RDS, the secrets, the schedule and the compute in your AWS account, and provisions an IAM role for the service. Machine sizes, autostop settings and health checks from fly.toml do not carry over; compute is configured per environment instead. If the app used a Fly volume for uploads, an object storage bucket is the usual replacement. The Fly.io migration notes cover private networking and multi-region setups.

When an agent is asked to move the next service off Fly, it works from Go declarations it can read, and checks them with encore run before the change is deployed.

Get started
brew install encoredev/tap/encore &&
encore app create
Copy

Run in your terminal to get started locally.

Manual path: ECS on Fargate and RDS

The Go binary and its Dockerfile can usually stay as they are, since Fly already builds from a Dockerfile. Make sure the final stage runs as a non-root user, for example with a distroless nonroot base image. On AWS you then create an ECR repository, an RDS instance in private subnets, an ECS cluster and service behind an Application Load Balancer, Secrets Manager entries referenced from the task definition, and the execution and task roles.

The scheduled Machine becomes an EventBridge Scheduler schedule that starts an ECS task running the cleanup command, with its own IAM role allowed to call ecs:RunTask and pass the task roles. Fly runs Machines in the regions you list; on AWS a second region means a second copy of the whole stack, so most teams pick one region for the move.

Good to know

Fly apps talk to each other over the private .internal network. Anything the Go API reaches that way, such as a Redis app, needs a new address on AWS or has to move too. Keep sslmode=require on the RDS connection string.

Moving the data

Stop writes, for example by scaling the app to zero Machines, then open a tunnel to the Postgres app with the Fly CLI (fly proxy 15432:5432 -a <postgres-app>) and dump through it with pg_dump --format=custom --no-owner --no-acl -h localhost -p 15432. With Encore the schema already exists from the migration, so restore only the data with pg_restore through a temporary connection string:

~/orders
$ encore db conn-uri orders --env=prod --write$ pg_restore --data-only --no-owner \ --dbname "$(encore db conn-uri orders --env=prod --write)" orders.dump$ curl https://prod-orders-xxxx.encr.app/orders/1{"id":1,"customer":"acme","totalCents":1999,"status":"paid","createdAt":"2026-10-25T11:51:10Z"}

The data-only restore also sets the orders_id_seq sequence, so new ids continue after the imported ones. For the manual path, restore into RDS from a machine inside the VPC, such as CloudShell in a VPC environment.

Switching DNS

Add the domain to the new environment, wait for the certificate, then update the DNS record. Fly apps often use an A and AAAA record pair pointing at Fly's anycast addresses, so replace both. Keep the Fly app scaled to zero and its Postgres app running for a few days before deleting them.

How to choose

Pick Encore when you want to keep shipping the Go API by pushing code, with Postgres, secrets and scheduled jobs defined in the Go code and running on RDS and Fargate in your own AWS account. The service can add Pub/Sub for work that ran in a separate process group on Fly.

Pick the manual ECS path when your AWS account is already managed with Terraform and this app should follow the same modules and review process.

Frequently asked questions

How do I migrate a Go app from Fly.io to AWS?

Map fly.toml services to a container service on ECS, Fly Postgres to RDS, Fly secrets to Secrets Manager, and scheduled Machines to EventBridge Scheduler. Copy the data with pg_dump through a local proxy to the Fly Postgres app, restore it with pg_restore, then switch DNS. With Encore, the database, secrets and cron jobs are declared in the Go code and provisioned in your AWS account on deploy.

How do I dump a Fly Postgres database?

Open a local tunnel to the Postgres app with the Fly CLI proxy command, then run pg_dump --format=custom against localhost with the database credentials. The result can be restored anywhere with pg_restore.

What replaces Fly volumes on AWS?

There is no direct equivalent for an app container. Move files to S3, structured data to Postgres, and use EBS or EFS only when the workload really needs a disk.

Deploy to your cloud on AWS and Google Cloud
without managing infrastructure

Encore lets developers and agents define application infrastructure directly in code, then automatically provisions it from local development to production in your AWS or GCP account.

~/orders — claude
Claude Code
Claude Code v2.1.180Opus 5.5~/orders