Fly.io is a clever way to deploy apps, but it surprises most people by making your app run everywhere by default, not just in one region.

Let’s see it in action. Imagine you have a simple Rails app. You’ve got your Gemfile, config/routes.rb, and a controller like this:

# app/controllers/pages_controller.rb
class PagesController < ApplicationController
  def home
    render plain: "Hello from Fly.io! Current time: #{Time.current}"
  end
end

And your routes:

# config/routes.rb
Rails.application.routes.draw do
  root 'pages#home'
end

First, you need the flyctl CLI. Install it, then log in:

flyctl auth login

Next, create a fly.toml file in your app’s root directory. This is your main configuration.

# fly.toml
app = "my-rails-app-unique-name" # Replace with a globally unique name
primary_region = "ord"

This fly.toml tells flyctl your app’s name and which region is the primary one. flyctl will use this to create the app on Fly.io.

flyctl deploy

After a few moments, flyctl will build your Docker image and deploy it. It will then give you a URL like my-rails-app-unique-name.fly.dev. Visit that URL, and you’ll see "Hello from Fly.io! Current time: [some time]".

Now, here’s where it gets interesting. If you run flyctl status, you’ll see something like:

App: my-rails-app-unique-name
...
Instances:
  ord (aws): some-id [v1] - 10.0.1.15 (grace)
  lhr (aws): another-id [v1] - 10.0.2.20 (grace)
  sfo (aws): yet-another-id [v1] - 10.0.3.25 (grace)

Your app is running in multiple regions (ord, lhr, sfo) simultaneously! Fly.io’s edge network automatically proxies requests to the nearest healthy instance of your app. This is why your app is fast for users worldwide. The primary_region in fly.toml is more about where new deployments start and where persistent volumes (like databases) might be located by default, not where your app only runs.

To manage this distributed deployment, you have a few key levers. The primary_region is one, but you can also specify experimental_regions in fly.toml to explicitly include or exclude regions from the automatic global distribution.

# fly.toml (example with more regions)
app = "my-rails-app-unique-name"
primary_region = "ord"
experimental_regions = ["lhr", "sfo", "sin", "gru"]

Running flyctl deploy again with this updated fly.toml would ensure your app is deployed to these specified regions. If you wanted to remove a region, you’d simply remove it from the experimental_regions list and redeploy.

The real magic is how Fly.io handles traffic. It uses Anycast networking. When a user requests my-rails-app-unique-name.fly.dev, their request is routed by the internet’s BGP (Border Gateway Protocol) to the Fly.io edge server closest to them. That edge server then forwards the request to the nearest healthy instance of your application, wherever it’s running. This is why latency is so low for users globally without you needing to configure complex load balancers or DNS.

What most people miss is how Fly.io manages persistent storage like databases in this multi-region setup. By default, primary_region dictates where a new fly postgres create command will provision a database. If your app needs to write to a database, and that database is in ord, but the user’s request is handled by an app instance in sfo, you’re going to have cross-region latency on writes. This is why database placement becomes a critical decision in a distributed architecture, and you often end up with a primary database in your primary_region and potentially read replicas in other regions, managed separately from the app deployment itself.

If you run flyctl logs, you’ll see logs from all running instances of your application, interleaved, giving you a unified view of your distributed system.

Want structured learning?

Take the full Fly-io course →