Fly.io Multi-Region Ruby with Postgres Read Replicas

For latency (or, less relevant to this article, data residency) reasons, you might want to deploy your app to a specific region. Fly.io has 20+ regions on their own Points of Presence (not on public clouds like AWS).

If you have a read-heavy application, look at Fly's multi-region Postgres read replicas. The big caveat is Fly, unlike Heroku, is not managed Postgres.

Ruby app

In this example, we'll write and deploy a Ruby app to Fly.io using popular gems:

The Gemfile looks like this:

source "https://rubygems.org"

ruby "~> 3"

gem "connection_pool"
gem "pg"
gem "puma"
gem "sinatra"

group :development, :test do
  gem "solargraph-standardrb"
  gem "standard"
end

Install the gems:

bundle

The api.rb looks like this:

require "connection_pool"
require "pg"
require "sinatra"

class DB
  def initialize
    connect
  end

  def exec(sql)
    do_exec(sql)
  rescue PG::ConnectionBad
    connect
    do_exec(sql)
  end

  private

  def connect
    url = ENV.fetch("DATABASE_URL")
    primary = ENV["PRIMARY_REGION"].to_s
    current = ENV["FLY_REGION"].to_s

    if primary != "" && current != "" && primary != current
      u = URI.parse(url)
      u.port = 5433
      url = u.to_s
    end

    @pool = ConnectionPool.new(size: 5, timeout: 5) {
      PG.connect(url)
    }
  end

  def do_exec(sql)
    @pool.with do |conn|
      conn.exec(sql)
    end
  end
end

db = DB.new

configure do
  set :protection, except: [:json_csrf]
end

get "/" do
  db.exec "SELECT 1"
  content_type :json
  {status: "ok"}.to_json
end

Develop

Develop locally:

bundle
createdb example_dev
DATABASE_URL="postgres:///example_dev" bundle exec ruby api.rb

Fly

Install the flyctl CLI, which is symlinked as fly:

brew install flyctl

Connect to your account:

fly auth login

Deploy using Fly's --remote-only option, which will use a remote Docker builder Fly sets up in your account. If you previously had a Docker installation, avoid a gotcha, by deleting ~/.docker before you deploy:

rm -rf ~/.docker
fly launch --remote-only

Configure region where the primary database is:

fly secrets set PRIMARY_REGION=sjc

Create the regions where the read replicas will go:

fly regions add ams lhr syd yul

Create read replicas in those regions:

fly volumes create pg_data -a example-read-replicas-db --size 1 --region ams
fly volumes create pg_data -a example-read-replicas-db --size 1 --region lhr
fly volumes create pg_data -a example-read-replicas-db --size 1 --region syd
fly volumes create pg_data -a example-read-replicas-db --size 1 --region yul

Scale 1 instance per region:

fly autoscale standard min=5 max=5

Done!