01/22/26

How to Migrate from Fly.io to GCP

Move from Fly.io to your own Google Cloud account

7 Min Read

Fly.io excels at global edge deployment. Your app runs close to users in 30+ regions. But you don't own the infrastructure, and integrating with cloud services requires networking workarounds. Migrating to GCP gives you infrastructure control and direct access to Google Cloud services.

Cloud Run provides similar developer experience to Fly.io: fast cold starts, automatic scaling, and no server management. The tradeoff is single-region deployment by default instead of automatic global distribution.

The traditional path to GCP means learning Terraform, 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 GCP project 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 GCP project using managed services like Cloud SQL, GCP Pub/Sub, and Cloud Storage.

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

The result is GCP 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 GCP ecosystem access. Companies like Groupon already use this approach to power their backends at scale.

What You're Migrating

Fly.io ComponentGCP Equivalent (via Encore)
Fly MachinesCloud Run
Fly PostgresCloud SQL
Fly VolumesGoogle Cloud Storage
Fly RegionsGCP Regions

Why GCP?

Cloud Run performance: Cold starts are often under 100ms for Node.js. Automatic scaling works similarly to Fly Machines.

Infrastructure ownership: You own the Cloud SQL database, GCS buckets, and Cloud Run services. Access them directly in GCP Console.

GCP ecosystem: BigQuery for analytics, Vertex AI for machine learning, Cloud CDN for caching.

Compliance capabilities: VPC, IAM, Cloud Armor, and audit logging.

The Global Edge Tradeoff

Fly.io deploys globally by default. GCP via Encore deploys to single regions. For many applications, this works fine. A well-configured Cloud Run service with Cloud CDN handles global traffic well.

If you genuinely need global edge deployment:

  • Single region + CDN: Works for most web applications
  • Multiple Encore environments: Deploy to different GCP regions and use Cloud Load Balancing
  • CloudFlare or Cloud CDN: Cache static content at edge locations

What Encore Handles For You

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

Encore follows GCP best practices and gives you guardrails. You can review infrastructure changes before they're applied, and everything runs in your own GCP project 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 Cloud SQL, GCS, Pub/Sub, and Cloud Scheduler 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.

Step 1: Migrate Your Application

Fly.io apps 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 [env] NODE_ENV = "production"

Encore:

import { api } from "encore.dev/api"; import { SQLDatabase } from "encore.dev/storage/sqldb"; const db = new SQLDatabase("main", { migrations: "./migrations" }); export const health = api( { method: "GET", path: "/health", expose: true }, async () => ({ status: "ok" }) ); export const getItems = api( { method: "GET", path: "/items", expose: true }, async (): Promise<{ items: Item[] }> => { const rows = await db.query<Item>`SELECT * FROM items ORDER BY created_at DESC`; const items: Item[] = []; for await (const item of rows) { items.push(item); } return { items }; } );

No configuration files. Encore provisions Cloud Run services with appropriate networking and scaling.

If you have multiple Fly apps (web + worker), create separate 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 Cloud SQL

Export from Fly Postgres

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

Set Up the Encore Database

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 Cloud SQL PostgreSQL when you deploy.

Put your migration files in ./migrations, or generate from your current schema:

pg_dump --schema-only "your-fly-connection" > migrations/001_initial.up.sql

Import to Cloud SQL

After your first Encore deploy:

# Get the Cloud SQL connection encore db conn-uri main --env=production # Import psql "your-cloud-sql-connection" < backup.sql

Step 3: Replace Fly Volumes with GCS

Fly Volumes are persistent disks. GCS is object storage, which is more scalable and accessible from any service.

Before (Fly with volumes):

import fs from "fs"; import path from "path"; // Write to mounted volume fs.writeFileSync("/data/uploads/file.pdf", buffer);

After (Encore with GCS):

import { Bucket } from "encore.dev/storage/objects"; const uploads = new Bucket("uploads", { versioned: false }); export const uploadFile = api( { method: "POST", path: "/upload", expose: true, auth: true }, async (req: { filename: string; data: Buffer; contentType: string }): Promise<{ url: 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 }): Promise<Buffer> => { return await uploads.download(filename); } );

Migrate Existing Files

# SSH into Fly machine fly ssh console -a my-app # Compress data tar -czvf /tmp/uploads.tar.gz /data/uploads # Download locally fly sftp get /tmp/uploads.tar.gz # Extract and upload to GCS after Encore deployment tar -xzvf uploads.tar.gz gsutil -m rsync -r ./uploads gs://your-encore-bucket

Step 4: Migrate Secrets

Fly 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"); const stripe = new Stripe(stripeKey());

Step 5: Handle Background Jobs

If your Fly app runs background workers, migrate to Pub/Sub:

import { Topic, Subscription } from "encore.dev/pubsub"; interface ProcessingJob { itemId: string; operation: string; } const jobQueue = new Topic<ProcessingJob>("jobs", { deliveryGuarantee: "at-least-once", }); // Enqueue jobs export const enqueueJob = api( { method: "POST", path: "/jobs", expose: true, auth: true }, async (req: ProcessingJob): Promise<{ queued: boolean }> => { await jobQueue.publish(req); return { queued: true }; } ); // Process jobs const _ = new Subscription(jobQueue, "processor", { handler: async (job) => { await processJob(job.itemId, job.operation); }, });

For scheduled jobs, use CronJob:

import { CronJob } from "encore.dev/cron"; export const dailyCleanup = api( { method: "POST", path: "/internal/cleanup" }, async () => { await cleanupOldRecords(); return { success: true }; } ); const _ = new CronJob("daily-cleanup", { title: "Clean up old records", schedule: "0 3 * * *", endpoint: dailyCleanup, });

Step 6: Deploy to GCP

  1. Connect GCP project in Encore Cloud. See the GCP setup guide for details.
  2. Push your code:
    git push encore main
  3. Run data migrations (database import, file sync)
  4. Update DNS
  5. Scale down Fly machines

What Gets Provisioned

  • Cloud Run for your services
  • Cloud SQL PostgreSQL for databases
  • Google Cloud Storage for objects
  • GCP Pub/Sub for messaging (if used)
  • Cloud Scheduler for cron (if used)
  • Cloud Logging for observability

Migration Checklist

  • Audit Fly.io app structure
  • Export Postgres database
  • Create Encore services
  • Set up migrations and import data
  • Convert Fly Volumes to GCS
  • Move secrets
  • Convert background jobs to Pub/Sub
  • Test in preview environment
  • Update DNS
  • Monitor
  • Scale down Fly machines

Wrapping Up

Cloud Run provides similar performance characteristics to Fly Machines: fast cold starts and automatic scaling. The tradeoff is single-region deployment instead of global distribution. For most applications, this works fine with Cloud CDN.

Ready to escape the maze of complexity?

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