Rails One Pager

Most of the documentation from the sidebar, but all on one big page so you can search your way to victory via Ctrl + F or Cmd + F.

Getting Started

In this guide we’ll develop and deploy a Rails application that first demonstrates a trivial view, then scaffolds a database table, and finally makes use of Turbo Streams to dynamically update pages.

In order to start working with Fly.io, you will need flyctl, our CLI app for managing apps. If you’ve already installed it, carry on. If not, hop over to our installation guide. Once that’s installed you’ll want to log in to Fly.

Once you have logged on, here are the three steps and a recap.

Rails Splash Screen

A newly generated Rails application will display a flashy splash screen when run in development, but will do absolutely nothing in production until you add code.

In order to demonstrate deployment of a Rails app on fly, we will create a new application, make a one line change that shows the splash screen even when run in production mode, and deploy the application.

Create an application

Start by verifying that you have Rails version 7 installed, and then by creating a new application:

$ rails --version
$ rails new list
$ cd list

Now use your favorite editor to make a one line change to config/routes.rb:

 Rails.application.routes.draw do
   # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html

   # Defines the root path route ("/")
-  # root "articles#index"
+  root "rails/welcome#index"
 end

Now that we have an application that does something, albeit something trivial, let’s deploy it.

Provision Rails and Postgres Servers

To configure and launch the app, you can use fly launch and follow the wizard. We are not going to use a database or redis yet, but say yes to both setting up a Postgresql database and to setting up Redis in order to prepare for the next step in this guide.

fly launch
Creating app in ~/list
Scanning source code
Detected a Rails app
? Choose an app name (leave blank to generate one): list
? Select Organization: John Smith (personal)
? Choose a region for deployment: Ashburn, Virginia (US) (iad)
Created app list in organization personal
Admin URL: https://fly.io/apps/list
Hostname: list.fly.dev
Set secrets on list: RAILS_MASTER_KEY
? Would you like to set up a Postgresql database now? Yes
For pricing information visit: https://fly.io/docs/about/pricing/#postgresql-clu
? Select configuration: Development - Single node, 1x shared CPU, 256MB RAM, 1GB disk
Creating postgres cluster in organization personal

. . .

Postgres cluster list-db is now attached to namelist
? Would you like to set up an Upstash Redis database now? Yes
? Select an Upstash Redis plan Free: 100 MB Max Data Size

Your Upstash Redis database namelist-redis is ready.

. . .

      create  Dockerfile
      create  .dockerignore
      create  bin/docker-entrypoint
      create  config/dockerfile.yml
Wrote config file fly.toml

Your Rails app is prepared for deployment.

Before proceeding, please review the posted Rails FAQ:
https://fly.io/docs/rails/getting-started/dockerfiles/.

Once ready: run 'fly deploy' to deploy your Rails app.

You can choose a name for your application or allow one to be assigned to you. You can choose where the application is run or a location near you will be picked. You can pick machine sizes for your database machine and redis; for demo purposes you can let these default. You can always change these later.

It is worth heeding the advice at the end of this: Before proceeding, please review the posted Rails FAQ: https://fly.io/docs/rails/getting-started/dockerfiles/.

Deploy your application

Deploying your application is done with the following command:

fly deploy

This will take a few seconds as it uploads your application, builds a machine image, deploys the images, and then monitors to ensure it starts successfully. Once complete visit your app with the following command:

fly apps open

That’s it! You are up and running! Wasn’t that easy?

If you have seen enough and are eager to get started, feel free to skip ahead to the Recap where you will see some tips.

For those that want to dig in deeper, let’s make the application a bit more interesting.

Scaffold to Success

Real Rails applications store data in databases, and Rails scaffolding makes it easy to get started. We are going to start with the simplest table possible, add a small bit of CSS to make the display a bit less ugly, and finally adjust our routes so that the main page is the index page for our new table.

Scaffold and style a list of names

Since we are focusing on fly deployment rather than Rails features, we will keep it simple and create a single table with exactly one column:

bin/rails generate scaffold Name name

Now add the following to the bottom of app/assets/stylesheets/application.css:

#names {
  display: grid;
  grid-template-columns: 1fr max-content;
  margin: 1em;
}

#names strong {
  display: none;
}

#names p {
  margin: 0.2em;
}

And finally, as the splash screen served it purpose, edit config/routes.rb once again, and replace the welcome screen with the names index:

 Rails.application.routes.draw do
   # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html

   # Defines the root path route ("/")
-  root "rails/welcome#index"
+  root "names#index"
 end

While certainly not fancy or extensive, we now have an application that makes use of a database.

Let’s deploy it.

Deployment

Normally at this point you have database migrations to worry about, code to push, and server processes to restart. Fly.io takes care of all of this and more, so all you need to do is the following:

$ fly deploy
$ fly apps open

Subsequent deploys are quicker than the first one as substantial portions of you application will have already been built.

Try it out! Add a few names and once you are done, proceed onto the final step.

Make Index come Alive

We now have a basic CRUD application where the index page shows a snapshot of the server state at the time it was displayed. Lets make the index page come alive using Turbo Streams.

This will involve provisioning a redis cluster and a surprisingly small number of updates to your application.

Adding turbo streams to your application.

There actually are five separate steps needed to make this work. Fortunately all but one require only a single line of code (or in one case, a single command). The third step actually requires two lines of code.

Start by generating a channel:

bin/rails generate channel names

Next, name the stream by modifying app/channels/names_channel.rb:

  class NamesChannel < ApplicationCable::Channel
    def subscribed
-      # stream_from "some_channel"
+      stream_from "names"
    end

    def unsubscribed
      # Any cleanup needed when channel is unsubscribed
    end
  end

Then modify app/views/names/index.html.erb to stream from that channel:

  <p style="color: green"><%= notice %></p>

  <h1>Names</h1>
+
+ <%= turbo_stream_from 'names' %>

  <div id="names">
    <% @names.each do |name| %>
      <%= render name %>
      <p>
        <%= link_to "Show this name", name %>
      </p>
    <% end %>
  </div>

  <%= link_to "New name", new_name_path %>
  </div>

And we complete the client changes by modifying app/views/names/_name.html.erb to identify the turbo frame:

- <div id="<%= dom_id name %>">
+ <%= turbo_frame_tag(dom_id name) do %>
    <p>
      <strong>Name:</strong>
      <%= name.name %>
    </p>

- </div>
+ <% end %>

There is only one step left, and that is to modify app/controllers/names_controller.rb to broadcast changes as updates are made:

  # PATCH/PUT /names/1 or /names/1.json
  def update
    respond_to do |format|
      if @name.update(name_params)
        format.html { redirect_to name_url(@name), notice: "Name was successfully updated." }
        format.json { render :show, status: :ok, location: @name }
+
+       @name.broadcast_replace_later_to 'names', partial: 'names/name'
      else
        format.html { render :edit, status: :unprocessable_entity }
        format.json { render json: @name.errors, status: :unprocessable_entity }
      end
    end
  end

Deployment and testing

By now it should be no surprise that deployment is as easy as fly deploy and fly apps open. Once that is done, copy the browser URL, open a second browser window (it can even be a different browser or even on a different machine), and paste the URL into the new window.

With one browser window open to the index page, use the other browser to change one of the names. Once you click “Update name” the index list in the original window will instantly update.

Of course, if this were a real application, inserting and removing names would cause those changes to be broadcast. As they say, this is left as an exercise for the student.

Arrived at Destination

You have successfully built, deployed, and connected to your first Rails application on Fly.

We’ve accomplished a lot with only just over a handful of lines of code and just over a dozen commands. When you are ready, proceed to a recap.

Recap

We started with an empty directory and in a matter of minutes had a running Rails application deployed to the web. A few things to note:

  • From a Rails perspective, we demonstrated Action Cable, Action Pack, Action View, Active Job, Active Model, Active Record, and Turbo Streams.
  • From a Fly.io perspective, we demonstrated deployment of a Rails app, a Postgres DB, a Redis cluster, and the setting of secrets.

Now that you have seen it up and running, a few things are worth noting:

  • No changes were required to your application to get it to work.
  • Your application is running on a VM, which starts out based on a docker image. To make things easy, fly launch generates a Dockerfile and a bin/docker-entrypoint for you which you are free to modify.
  • There is also a config/dockerfile.yml file which keeps track of your dockerfile generation options. This was covered by the FAQ you read above.
  • Other files of note: .dockerignore and fly.toml, both of which you can also modify. All five files should be checked into your git repository.
  • fly dashboard can be used to monitor and adjust your application. Pretty much anything you can do from the browser window you can also do from the command line using fly commands. Try fly help to see what you can do.
  • fly ssh console can be used to ssh into your VM. fly ssh console --pty -C "/rails/bin/rails console" can be used to open a rails console.

Now that you have seen how to deploy a trivial application, it is time to move on to The Basics.


Dockerfiles and fly.toml

Once you have completed running fly launch you have some new files, most notably a Dockerfile and a fly.toml file. For many applications you are ready to deploy. But before you do, scan the following list to see if any of these common situations apply to you, and how to proceed.

If after reading this and you still need help, please post on community.fly.io. We also offer email support, for the apps you really care about.

Updates

Your application is unlikely to stay the same forever. Perhaps you’ve updated to a new version of Ruby, bundler, node, or other package. Or added a gem which has system dependencies. When this occurs, you will need to update your Dockerfile to match. In most cases, all you need to do is rerun the generator:

bin/rails generate dockerfile

The generator will remember the options you selected before (these are stored in config/dockerfile.yml). If you need to change a boolean option, add or remove a no- prefix before the option name.

If you have made hand edits to your Dockerfile you may want to take advantage of the option to diff the changes before they are applied.

Custom Packages

The Dockerfile generator for Rails attempts to detect common dependencies and handle them for you. If you come across a dependency that may be useful to others, please consider opening up an issue or a pull request.

You may have needs beyond what is automatically detected. Most official Ruby docker images are based on Debian bullseye, and there are a large number of packages available to be installed in this manner.

An example adding basic kernel and network monitoring packages from this list:

bin/rails generate dockerfile --add procps net-tools traceroute iputils-ping

Using Sqlite3

Every time you deploy you will start out with a fresh image. If your database is on that image, it too will start fresh which undoubtedly is not what you want.

The solution is to create a Fly Volume.

Once you have created a volume, you will need to set the DATABASE_URL environment variable to cause Rails to put your database on that volume. The result will be the following lines in your fly.toml file:

[env]
  DATABASE_URL = "sqlite3:///mnt/volume/production.sqlite3"

[mounts]
  source = "sqlite3_volume"
  destination = "/mnt/volume"

Adjust the name of the source to match the name of the volume you created.

Out of Memory

RAM is a precious commodity - both to those on Hobby plans who want to remain within or near the free allowances, and to apps that want to scale to be able to handle a large number of concurrent connections.

Both fullstaq and jemalloc are used by many to reduce their memory footprint. As every application is different, test your application to see if either are appropriate for you. Enabling one or both can be done by regenerating your Dockerfile and specifying the appropriate option(s):

bin/rails generate dockerfile --fullstaq --jemalloc

At some point you may find that you need more memory. There are two types: real and virtual. Real is faster, but more expensive. Virtual is slower but free.

To scale your app to 1GB of real memory, use:

fly scale memory 1024

To allocate 1GB of swap space for use as virtual memory, add the following to your fly.toml:

swap_size_mb = 1024

Scaling

If your application involves multiple servers, potentially spread across a number of regions, you will want to prepare your databases once per deploy not once per server.

Regenerate your Dockerfile specifying that you no longer want the prepare step there:

bin/rails generate dockerfile --no-prepare

Next, add a deploy step to your fly.toml:

[deploy]
  release_command = "bin/rails db:prepare"

Shelling in

Fly provides the ability to ssh into your application, and it would be convenient to run things like the Rails console in one line:

fly ssh console --pty -C '/rails/bin/rails console'

To enable bin/rails commands to be run in this manner, adjust your deployed binstubs to set the current working directory:

bin/rails generate dockerfile --bin-cd

Build speeds

The Dockerfile you were provided will only install gems and node modules if files like Gemfile and package.json have been modified. If you are finding that you are doing this often and deploy speed is important to you, turning on build caching can make a big difference. And if your Rails application makes use of node.js, installing gems and node packages in parallel can reduce build time. You can regenerate your Dockerfile to enable one or both:

bin/rails generate dockerfile --cache --parallel

Runtime performance

Ruby images starting with 3.2 include YJIT but disabled. You can enable YJIT using:

bin/rails generate dockerfile --yjit

Testing Locally

If you have Docker installed locally, you can test your applications before you deploy them by running the following commands:

bin/rails generate dockerfile --compose
export RAILS_MASTER_KEY=$(cat config/master.key)
docker compose build
docker compose up

Windows PowerShell users will want to use the following command instead of export:

$Env:RAILS_MASTER_KEY = Get-Content 'config\master.key'

Migrate from Heroku

This guide runs you through how to migrate a basic Rails application off of Heroku and onto Fly. It assumes you’re running the following services on Heroku:

  • Puma web server
  • Postgres database
  • Redis in non-persistent mode
  • Custom domain
  • Background worker, like Sidekiq

If your application is running with more services, additional work may be needed to migrate your application off Heroku.

Migrating your app

The steps below run you through the process of migrating your Rails app from Heroku to Fly.

Provision and deploy Rails app to Fly

From the root of the Rails app you’re running on Heroku, run fly launch and select the options to provision a new Postgres database, and optionally a Redis database if you make use of Action Cable, caching, and popular third party gems like Sidekiq.

fly launch
Creating app in ~/list
Scanning source code
Detected a Rails app
? Choose an app name (leave blank to generate one): list
? Select Organization: John Smith (personal)
? Choose a region for deployment: Ashburn, Virginia (US) (iad)
Created app list in organization personal
Admin URL: https://fly.io/apps/list
Hostname: list.fly.dev
Set secrets on list: RAILS_MASTER_KEY
? Would you like to set up a Postgresql database now? Yes
For pricing information visit: https://fly.io/docs/about/pricing/#postgresql-clu
? Select configuration: Development - Single node, 1x shared CPU, 256MB RAM, 1GB disk
Creating postgres cluster in organization personal

. . .

Postgres cluster list-db is now attached to namelist
? Would you like to set up an Upstash Redis database now? Yes
? Select an Upstash Redis plan Free: 100 MB Max Data Size

Your Upstash Redis database namelist-redis is ready.

. . .

      create  Dockerfile
      create  .dockerignore
      create  bin/docker-entrypoint
      create  config/dockerfile.yml
Wrote config file fly.toml

Your Rails app is prepared for deployment.

Before proceeding, please review the posted Rails FAQ:
https://fly.io/docs/rails/getting-started/dockerfiles/.

Once ready: run 'fly deploy' to deploy your Rails app.

It is worth heeding the advice at the end of this: Before proceeding, please review the posted Rails FAQ: https://fly.io/docs/rails/getting-started/dockerfiles/.

After the application is provisioned, deploy it by running:

fly deploy

When that’s done, view your app in a browser:

fly apps open

There’s still work to be done to move more Heroku stuff over, so don’t worry if the app doesn’t boot right away. There’s a few commands that you’ll find useful to configure your environment:

  • fly logs - Read error messages and stack traces emitted by your Rails application.
  • fly ssh console --pty -C "/rails/bin/rails console" - Launches a Rails shell, which is useful to interactively test components of your Rails application.

Transfer Heroku secrets

To see all of your Heroku env vars and secrets, run:

heroku config -s | grep -v -e "RAILS_MASTER_KEY" -e "DATABASE_URL" -e "REDIS_URL" -e "REDIS_TLS_URL" | fly secrets import

This command exports the Heroku secrets, excluding RAILS_MASTER_KEY, DATABASE_URL REDIS_URL, and REDIS_TLS_URL, and imports them into Fly.

Verify your Heroku secrets are in Fly.

fly secrets list
NAME                          DIGEST                            CREATED AT
DATABASE_URL                  24e455edbfcf1247a642cdae30e14872  14m29s ago
LANG                          95a7bb7a8d0ee402edde95bb78ef95c7  1m24s ago
RACK_ENV                      fd89784e59c72499525556f80289b2c7  1m26s ago
RAILS_ENV                     fd89784e59c72499525556f80289b2c7  1m26s ago
RAILS_LOG_TO_STDOUT           a10311459433adf322f2590a4987c423  1m25s ago
RAILS_SERVE_STATIC_FILES      a10311459433adf322f2590a4987c423  1m23s ago
REDIS_TLS_URL                 b30fe87493e14d9b670dc0263dc935c9  1m25s ago
REDIS_URL                     4583a46e747696319573e8bfbd0db04d  1m21s ago
SECRET_KEY_BASE               5afb43c2ddbba6c02ffa7e2834689692  1m22s ago

Transfer the Database

Any new data created by your Heroku app during this database migration won’t be moved over to Fly. Consider taking your Heroku application offline or place in read-only mode if you want to be confident that this migration will move over 100% of your Heroku data to Fly.

Set the HEROKU_DATABASE_URL variable in your Fly environment.

fly secrets set HEROKU_DATABASE_URL=$(heroku config:get DATABASE_URL)

Alright, lets start the transfer remotely on the Fly instance.

fly ssh console

Then from the remote Fly SSH console transfer the database.

pg_dump -Fc --no-acl --no-owner -d $HEROKU_DATABASE_URL | pg_restore --verbose --clean --no-acl --no-owner -d $DATABASE_URL

You may need to upgrade your Heroku database to match the version of the source Fly database. Refer to Heroku’s Upgrading the Version of a Heroku Postgres Database for instructions on how to upgrade, then try the command above again.

After the database transfers unset the HEROKU_DATABASE_URL variable.

fly secrets unset HEROKU_DATABASE_URL

Then launch your Heroku app to see if its running.

fly apps open

If you have a Redis server, there’s a good chance you need to set that up.

Multiple processes & background workers

Heroku uses Procfiles to describe multi-process Rails applications. Fly describes multi-processes with the [processes] directive in the fly.toml file.

If your Heroku Procfile looks like this:

web: bundle exec puma -C config/puma.rb
worker: bundle exec sidekiq
release: rails db:migrate

Move everything except for the release: line to your fly.toml file:

[processes]
web = "bundle exec puma -C config/puma.rb"
worker = "bundle exec sidekiq"

If you have a release: line in your Heroku Procfile, that will listed separately in your fly.toml file:

[deploy]
  release_command = "bin/rails db:migrate"

You will also want to prevent your release command from also being run during the deploy step. To do so, regenerate your dockerfile using:

$ bin/rails generate dockerfile --no-prepare

Next, under the [[services]] directive, find the entry that maps to internal_port = 8080, and add processes = ["web"]. The configuration file should look something like this:

[[services]]
  processes = ["web"] # this service only applies to the web process
  http_checks = []
  internal_port = 8080
  protocol = "tcp"
  script_checks = []

This associates the process with the service that Fly launches. Save these changes and run the deploy command.

fly deploy

You should see a web and worker process deploy.

Custom Domain & SSL Certificates

After you finish deploying your application to Fly and have tested it extensively, read through the Custom Domain docs and point your domain at Fly.

In addition to supporting CNAME DNS records, Fly also supports A and AAAA records for those who want to point example.com (without the www.example.com) directly at Fly.

Cheat Sheet

Old habits die hard, especially good habits like deploying frequently to production. Below is a quick overview of the differences you’ll notice initially between Fly and Heroku.

Commands

Fly commands are a bit different than Heroku, but you’ll get use to them after a few days.

Task Heroku Fly
Deployments git push heroku fly deploy
Rails console heroku console fly ssh console --pty -C "/app/bin/rails console"
Database migration heroku rake db:migrate fly ssh console -C "/app/bin/rake db:migrate"
Postgres console heroku psql fly postgres connect -a <name-of-database-app-server>
Tail log files heroku logs fly logs
View configuration heroku config fly ssh console -C "printenv"
View releases heroku releases fly releases
Help heroku help fly help

Check out the Fly CLI docs for a more extensive inventory of Fly commands.

Deployments

By default Heroku deployments are kicked off via the git push heroku command. Fly works a bit differently by kicking of deployments via fly deploy—git isn’t needed to deploy to Fly. The advantage to this approach is your git history will be clean and not full of commits like git push heroku -am "make app work" or git push heroku -m "ok it will really work this time".

To achieve the desired git push behavior, we recommend setting up fly deploy as the final command in your continuous integration pipeline, as outlined for GitHub in the Continuous Deployment with Fly and GitHub Actions docs.

Release phase tasks

Heroku has a release: rake db:migrate command in their Procfiles to run tasks while the application is deployed. Rails 7.1 will include a bin/rails db:prepare in the list of commands to be run on deploy in their bin/docker-entrypoint file. Fly.io supports both approaches.

If you don’t want to run migrates by default per release, delete the prequite but leave the :release task. You’ll be able to manually run migrations on Fly via fly ssh console -C "/app/bin/rails db:migrate".

Deploy via git

Heroku’s default deployment technique is via git push heroku. Fly doesn’t require a git commit, just run fly deploy and the files on your local workstation will be deployed.

Fly can be configured to deploy on git commits with the following techniques with a GitHub Action.

Databases

Fly and Heroku have different Postgres database offerings. The most important distinction to understand about using Fly is that it automates provisioning, maintenance, and snapshot tasks for your Postgres database, but it does not manage it. If you run out of disk space, RAM, or other resources on your Fly Postgres instances, you’ll have to scale those virtual machines from the Fly CLI.

Contrast that with Heroku, which fully manages your database and includes an extensive suite of tools to provision, backup, snapshot, fork, patch, upgrade, and scale up/down your database resources.

The good news for people who want a highly managed Postgres database is they can continue hosting it at Heroku and point their Fly instances to it!

Heroku’s managed database

One command is all it takes to point Fly apps at your Heroku managed database.

fly secrets set DATABASE_URL=$(heroku config:get DATABASE_URL)

This is a great way to get comfortable with Fly if you prefer a managed database provider. In the future if you decide you want to migrate your data to Fly, you can do so pretty easily with a few commands.

Fly’s databases

The most important thing you’ll want to be comfortable with using Fly’s database offering is backing up and restoring your database.

As your application grows, you’ll probably first scale disk and RAM resources, then scale out with multiple replicas. Common maintenance tasks will include upgrading Postgres as new versions are released with new features and security updates.

You Postgres, now what? is a more comprehensive guide for what’s required when running your Postgres databases on Fly.

Pricing

Heroku and Fly have very different pricing structures. You’ll want to read through the details on Fly’s pricing page before launching to production. The sections below serve as a rough comparison between Heroku and Fly’s plans as of August 2022.

Please do your own comparison of plans before switching from Heroku to Fly. The examples below are illustrative estimates between two very different offerings, which focuses on the costs of app & database servers. It does not represent the final costs of each plan. Also, the prices below may not be immediately updated if Fly or Heroku change prices.

Free Plans

Heroku will not offer free plans as of November 28, 2022.

Fly offers free usage for up to 3 full time VMs with 256MB of RAM, which is enough to run a tiny Rails app and Postgres database to get a feel for how Fly works.

Plans for Small Rails Apps

Heroku’s Hobby tier is limited to 10,000 rows of data, which gets exceeded pretty quickly requiring the purchase of additional rows of data.

Heroku Resource Specs Price
App Dyno 512MB RAM $7/mo
Database 10,000,000 rows $9/mo
Estimated cost $16/mo

Fly’s pricing is metered for the resources you use. Database is billed by the amount of RAM and disk space used, not by rows. The closest equivalent to the Heroku Hobby tier on Fly looks like this:

Fly Resource Specs Price
App Server 1GB RAM ~$5.70/mo
Database Server 256MB RAM / 10Gb disk ~$3.44/mo
Estimated cost ~$9.14/mo

Plans for Medium to Large Rails Apps

There’s too many variables to compare Fly and Heroku’s pricing for larger Rails applications depending on your needs, so you’ll definitely want to do your homework before migrating everything to Fly. This comparison focuses narrowly on the costs of app & database resources, and excludes other factors such as bandwidth costs, bundled support, etc.

Heroku Resource Specs Price Quantity Total
App Dyno 2.5GB RAM $250/mo 8 $2,000/mo
Database 61GB RAM / 1TB disk $2,500/mo 1 $2,500/mo
Estimated cost $4,500/mo

Here’s roughly the equivalent resources on Fly:

Fly Resource Specs Price Quantity Total
App Server 4GB RAM / 2X CPU ~$62.00/mo 8 ~$496/mo
Database Server 64GB RAM / 500GB disk ~$633/mo 2 ~$1,266/mo
Estimated cost ~$1,762/mo

Again, the comparison isn’t realistic because it focuses only on application and database servers, but it does give you an idea of how the different cost structures scale on each platform. For example, Heroku’s database offering at this level is redundant, whereas Fly offers 2 database instances to achieve similar levels of redundancy.


Existing Rails Apps

If you have an existing Rails app that you want to move over to Fly, this guide walks you through the initial deployment process and shows you techniques you can use to troubleshoot issues you may encounter in a new environment.

Provision Rails and Postgres Servers

To configure and launch your Rails app, you can use fly launch and follow the wizard.

fly launch
Creating app in ~/list
Scanning source code
Detected a Rails app
? Choose an app name (leave blank to generate one): list
? Select Organization: John Smith (personal)
? Choose a region for deployment: Ashburn, Virginia (US) (iad)
Created app list in organization personal
Admin URL: https://fly.io/apps/list
Hostname: list.fly.dev
Set secrets on list: RAILS_MASTER_KEY
? Would you like to set up a Postgresql database now? Yes
For pricing information visit: https://fly.io/docs/about/pricing/#postgresql-clu
? Select configuration: Development - Single node, 1x shared CPU, 256MB RAM, 1GB disk
Creating postgres cluster in organization personal

. . .

Postgres cluster list-db is now attached to namelist
? Would you like to set up an Upstash Redis database now? Yes
? Select an Upstash Redis plan Free: 100 MB Max Data Size

Your Upstash Redis database namelist-redis is ready.

. . .

      create  Dockerfile
      create  .dockerignore
      create  bin/docker-entrypoint
      create  config/dockerfile.yml
Wrote config file fly.toml

Your Rails app is prepared for deployment.

Before proceeding, please review the posted Rails FAQ:
https://fly.io/docs/rails/getting-started/dockerfiles/.

Once ready: run 'fly deploy' to deploy your Rails app.

You can set a name for the app, choose a default region, and choose to launch and attach either or both a PostgreSQL and Redis databases. Be sure to include Redis is if you make use of Action Cable, caching, and popular third-party gems like Sidekiq.

Deploy your application

Deploying your application is done with the following command:

fly deploy

This will take a few seconds as it uploads your application, builds a machine image, deploys the images, and then monitors to ensure it starts successfully. Once complete visit your app with the following command:

fly apps open

If all went well, you’ll see your Rails application homepage.

Troubleshooting your initial deployment

Since this is an existing Rails app, its highly likely it might not boot because you probably need to configure secrets or other service dependencies. Let’s walk through how to troubleshoot these issues so you can get your app running.

View log files

If your application didn’t boot on the first deploy, run fly logs to see what’s going on.

fly logs

This shows the past few log file entries and tails your production log files.

Rails stack tracebacks can be lengthy, and the information you often want to see is at the top. If not enough information is available in the fly logs command, try running fly dashboard, and select Monitoring in the left-hand column.

Open a Rails console

It can be helpful to open a Rails console to run commands and diagnose production issues. If you are not running with a sqlite3 or a volume, the recommended way to do this is to run a console in an ephemeral Machine:

fly console

If you are running with sqlite3 or a volume, you will need to ssh into an existing machine. You may need to first make sure that you have enough memory to accommodate the additional session.

fly ssh console --pty -C "/rails/bin/rails console"
Loading production environment (Rails 7.0.4.2)
irb(main):001:0> 

Common initial deployment issues

Now that you know the basics of troubleshooting production deployments, lets have a look at some common issues people have when migrating their existing Rails applications to Fly.

Access to Environment Variables at Build Time

Some third-party gems and services require configuration including the setting of secrets/environment variables. The assets:precompile step will load your configuration and may fail if those secrets aren’t set even if they aren’t actually used by the assets:precompile step.

Rails itself has such a variable, and you will see some combination of SECRET_KEY_BASE and DUMMY in most Dockerfiles.

In many cases, you can avoid the problem by adding an if statement. For example, if your code looks like:

Stripe.api_key = Rails.application.credentials.stripe[:secret_key]

Changing the configuration file to the following will avoid the build time error:

if Rails.application.credentials.stripe
  Stripe.api_key = Rails.application.credentials.stripe[:secret_key]
end

If that is not sufficient and you need more such dummy values, add them directly to the Dockerfile. Just be sure that any such values you add to your Dockerfile don’t contain actual secrets as your Dockerfile will generally be committed to git or otherwise may be visible.

If you have need for actual secrets at build time, take a look at Build Secrets.

Finally, if there are no other options you can generate a Dockerfile that will run assets:precompile at deployment time with the following command:

bin/rails generate dockerfile --precompile=defer

This will result in larger images that are slower to deploy:

  • The precompile step will be run for each server you deploy rather that once during build time to produce an image that can be deployed multiple times.
  • Normally Dockerfiles are structured so that packages that are only needed at build time (e.g. Node.js) are not present on the deployed machine.
    If you defer the assets:precompile step, these packages will need to be present in order to deploy.

If you are evaluating Fly.io for the first time there may be some value in setting precompile to defer initially for evaluation and then work over time to eliminate the issues that prevent you from running this step at build time. Once those issues are resolved, regenerate your Dockerfile using the following command:

bin/rails generate dockerfile --precompile=build

Language Runtime Versions

Having different runtime versions of language runtimes on your development machine and on production VMs can lead to problems. Run the following commands to see what versions you are using in development:

$ bundle -v
$ node -v
$ ruby -v
$ yarn -v

There also are files used by version managers to keep your development environment in sync: .ruby-version, and .node-version.

Finally, package.json files may have version numbers in engines.node and packageManager values.

Whenever you update your tools, run the following command to update your Dockerfile:

$ bin/rails generate dockerfile

You can see the versions of each tool that will be used on your deployment machine by looking for lines that start with ARG in your Dockerfile.

ActiveStorage

From the documentation:

Active Storage facilitates uploading files to a cloud storage service like Amazon S3, Google Cloud Storage, or Microsoft Azure Storage and attaching those files to Active Record objects. It comes with a local disk-based service for development and testing and supports mirroring files to subordinate services for backups and migrations.

Accordingly:

Litefs is currently in beta, and if there were a sqlite3 active storage adapter it could be used for this purpose. If this is of interest, bring up the topic on community.fly.io.

Postgres database drivers

If you didn’t initially deploy with a postgres database but want to add one later, you can create a database using fly postgres create. Next, update your dockerfile to include the postgres libraries using:

$ bin/rails generate dockerfile --postgresql

Finally, attach the database to your application using fly postgres attach.

Multiple Rails applications can use the same PostgresQL server. Just take care to make sure that each Rails application uses a different database name.

ActiveSupport::MessageEncryptor::InvalidMessage

Generally, this means that there is a problem with your RAILS_MASTER_KEY. It is a common initial setup problem, but once it works it tends to keep working.

fly launch will extract your master key if your project has one and make it available to your deployed application as a secret.

If you’ve already run fly launch on a project that doesn’t have a master key (commonly because files containing these values are excluded from being pushed by being listed in your .gitignore file), you will need to generate a key and set the secret yourself. The Ruby on Rails Guides contains information on generating new credentials.

If you’ve got your app’s secrets stored in an encrypted credentials file such as config/credentials.yml.enc or config/credentials/production.yml.enc, you’ll need to provide the master key to your app via fly secrets set. For example, if your master key is stored in config/master.key, you can run:

fly secrets set RAILS_MASTER_KEY=$(cat config/master.key)

Windows users can run the following command in PowerShell:

fly secrets set RAILS_MASTER_KEY=$(Get-Content config\master.key)

You can verify that your credentials are encoded using your current master key using:

bin/rails credentials:show

You can see what RAILS_MASTER_KEY is deployed using:

fly ssh console -C 'printenv RAILS_MASTER_KEY'

Experimental Fly Rails Gem

This guide runs you through how to use the fly-rails gem to deploy your Rails applications to Fly.io using Rails commands.

Please note that the fly-rails gem is designed to work well with common Rails production configurations, like a Turbo app that uses Sidekiq, AnyCable, Redis, & Postgres. For less common configurations, we recommend using flyctl and its respective configuration files to manage your deployment.

Install the flyctl command line interface

First you’ll need to install the Fly CLI, and signup for a Fly.io account. The gem used in the next step will use this CLI to deploy your application to Fly.io and it’s something that’s worth learning more about as you become more comfortable with Fly.io.

Install the gem

Add the fly-rails gem from the root of your Rails project.

bundle add fly-rails

This installs and bundles the fly-rails gem in your Rails project, which adds commands to Rails that make deploying apps to Fly.io require less steps.

Launch your app

The first deployment will both provision and deploy the application to Fly.io. Run the following command:

bin/rails fly:launch

If the deployment is successful, you can view your app by running:

fly apps open

You should see your application!

Deployments

Changes to your application can be deployed with one command.

bin/rails fly:deploy

This will deploy the latest changes to your application and run any database migrations, if present.

Ejecting

There may be a point where you need to “eject” from the fly-rails gem for deployments that require more advanced configuration. Ejecting creates a fly.toml and Dockerfile at the root of your project that works with flyctl and gives you full and exact control over your application configuration.

bin/rails generate fly:app --eject

Most of the documentation from the sidebar, but all on one big page so you can search your way to victory via Ctrl + F or Cmd + F.

The Basics

Most Rails applications require additional setup beyond provisioning a Postgres and Redis database. These guides will help you get through the basics of setting up your Rails application with pieces of infrastructure commonly found in medium-to-high complexity Rails applications.


Turbo Streams & Action Cable

Deploying Turbo Streams or Action Cable with a Rails app involves provisioning a redis cluster and a few updates to your application.

Provisioning Redis

Before proceeding, verify that your application is already set up to use Redis. Examine your Gemfile and look for the following lines:

# Use Redis adapter to run Action Cable in production
gem "redis", "~> 4.0"

If the second line is commented out, uncomment it and then run bundle install. Rails will automatically have done this for you if it detected the redis-server executable on your machine at that time the application was created.

Now that Rails is ready to make use of Redis, lets deploy a redis cluster:

fly redis create
? Select Organization: John Smith (personal)
? Choose a Redis database name (leave blank to generate one): list-redis
? Choose a primary region (can't be changed later) Ashburn, Virginia (US) (iad)
? Optionally, choose one or more replica regions (can be changed later):

Upstash Redis can evict objects when memory is full. This is useful when caching in Redis. This setting can be changed later.
Learn more at https://fly.io/docs/reference/redis/#memory-limits-and-object-eviction-policies
? Would you like to enable eviction? No
? Select an Upstash Redis plan Free: 100 MB Max Data Size

Your Upstash Redis database list-redis is ready.
Apps in the personal org can connect to at redis://default:<redacted>.upstash.io
If you have redis-cli installed, use fly redis connect to connect to your database.

Once again, you can set a name for the database, chose a primary region as well as a number of replica regions, enable eviction, and select a plan.

The most important line in this output is the second to the last one which will contain a URL starting with redis:. The URL you see will be considerably longer than the one you see above. You will need to provide this URL to Rails, and with Fly.io this is done via secrets. Run the following command replacing the url with the one from the output above:

fly secrets set REDIS_URL=redis://default:<redacted>.upstash.io

Now you are ready. Rails is set up to use redis, knows where to find the redis instance, and the instance is deployed. Now onto the implementation:

Adding Turbo Streams to your Application

There are very few steps to make this work, writing very few lines of code

Let’s start with the turbo_stream_for helper, which under the hood uses Turbo::StreamsChannel.

Modify app/views/names/index.html.erb to stream from “names”:

  <p style="color: green"><%= notice %></p>

  <h1>Names</h1>
+
+ <%= turbo_stream_from 'names' %>

  <div id="names">
    <% @names.each do |name| %>
      <%= render name %>
      <p>
        <%= link_to "Show this name", name %>
      </p>
    <% end %>
  </div>

  <%= link_to "New name", new_name_path %>
  </div>

And we complete the client changes by modifying app/views/names/_name.html.erb to identify the turbo frame:

- <div id="<%= dom_id name %>">
+ <%= turbo_frame_tag(dom_id name) do %>
    <p>
      <strong>Name:</strong>
      <%= name.name %>
    </p>

- </div>
+ <% end %>

There is only one step left, and that is to modify app/controllers/names_controller.rb to broadcast changes as updates are made:

  # PATCH/PUT /names/1 or /names/1.json
  def update
    respond_to do |format|
      if @name.update(name_params)
        format.html { redirect_to name_url(@name), notice: "Name was successfully updated." }
        format.json { render :show, status: :ok, location: @name }
+
+       @name.broadcast_replace_later_to 'names', partial: 'names/name'
      else
        format.html { render :edit, status: :unprocessable_entity }
        format.json { render json: @name.errors, status: :unprocessable_entity }
      end
    end
  end

Deployment and testing

By now it should be no surprise that deployment is as easy as fly deploy and fly apps open. Once that is done, copy the browser URL, open a second browser window (it can even be a different browser or even on a different machine), and paste the URL into the new window.

With one browser window open to the index page, use the other browser to change one of the names. Once you click “Update name” the index list in the original window will instantly update.

Of course, if this were a real application, inserting and removing names would cause those changes to be broadcast. As they say, this is left as an exercise for the student.

Arrived at Destination

You have successfully built, deployed, and connected to your first Rails application on Fly.io.

We’ve accomplished a lot with only just over a handful of lines of code and just over a dozen commands. When you are ready, proceed to a recap.


Deployments

Deploying applications to Fly can be as simple as running:

fly deploy

When the application successfully deploys, you can quickly open it in the browser by running:

fly apps open

If all goes well, you should see a running application in your web browser. You can also view a history of deployments by running:

fly releases
VERSION STABLE  TYPE  STATUS    DESCRIPTION                             USER                  DATE
v55     true    scale succeeded Scale VM count: ["web, 6"]              brad@fly.io           2022-08-10T17:05:57Z
v54     true    scale dead      Scale VM count: ["web, 0"]              brad@fly.io           2022-08-10T16:43:13Z
v53     true    scale succeeded Scale VM count: ["web, 6"]              brad@fly.io           2022-08-10T16:42:51Z
v52     true    scale succeeded Scale VM count: ["web, 6"]              brad@fly.io           2022-08-10T16:40:57Z
v51     true    scale succeeded Scale VM count: ["web, 3"]              kurt@fly.io           2022-08-08T20:14:08Z
v50     true    scale succeeded Scale VM count: ["web, 3"]              kurt@fly.io           2022-08-08T19:55:23Z

Troubleshooting a deployment

If a deployment fails, you’ll see an error message in the console. If the error is a Rails stack trace, it will be truncated. To view the entire error message run:

fly logs

You may need to open another terminal window and deploy again while running fly logs to see the full error message.

Running migrations

Migrations are configured to automatically run after each deployment via the following task in your application’s lib/tasks/fly.rake file:

task :release => 'db:migrate'

To disable automatic migrations for deploys, remove the dependency from the :release task. Then, to manually run migrations after a deployment, run:

fly ssh console -C "/rails/bin/rails db:migrate"

Run ad-hoc tasks after deploying

Sometimes after a deployment you’ll need to run a script or migration in production. That can be accomplished with the Fly SSH console by running:

fly ssh console
Connecting to top1.nearest.of.my-rails-app.internal... complete
cd app
ls
Aptfile       CHANGELOG.md  Dockerfile    LICENSE     README.md  app   config.ru  fly     package.json       pull_request_template.md  test    yarn.lock
Brewfile      CODE_OF_CONDUCT.md  Gemfile       Procfile      Rakefile   bin   db     lib     postcss.config.js  resources           tmp
Brewfile.lock.json  CONTRIBUTING.md Gemfile.lock  Procfile.dev  SECURITY.md  config  docs     node_modules  public       tailwind.config.js        vendor
bundle exec ruby my-hello-world-script.rb
hello world

Asset compilation and build commands

The default Rails image is configured to run assets:precompile in your application’s lib/tasks/fly.rake file:

task :build => 'assets:precompile'

If you have additional build steps beyond the Rails asset precompiler, you may need to modify your application’s lib/tasks/fly.rake file.


Running Tasks & Consoles

Rails console

To access an interactive Rails console, run:

fly ssh console --pty -C "/rails/bin/rails console"
Loading production environment (Rails 7.0.4.2)
irb(main):001:0>

Then start using the console, but be careful! You’re in a production environment.

The --pty flag tells the SSH server to run the command in a pseudo-terminal. You will generally need this only when running interactive commands, like the Rails console.

Interactive shell

To access an interactive shell as the root user, run:

fly ssh console --pty -C '/bin/bash'
#

To access an interactive shell as the rails user, requires a tiny bit of setup:

bin/rails generate dockerfile --sudo

Accept the changes to your Dockerfile, and then rerun fly deploy.

Once this is complete, you can create an interactive shell:

fly ssh console --pty -C 'sudo -iu rails'
$

Rails tasks

In order to run other Rails tasks, a small change is needed to your Rails binstubs to set the current working directory. The following command will make the adjustment for you:

bin/rails generate dockerfile --bin-cd

Accept the changes to your Dockerfile, and then rerun fly deploy.

Once this is complete, you can execute other commands on Fly, for example:

fly ssh console -C "/rails/bin/rails db:migrate"

To list all the available tasks, run:

fly ssh console -C "/rails/bin/rails help"

Custom Rake tasks

You can create Custom Rake Tasks to automate frequently used commands. As an example, add the following into lib/tasks/fly.rake to reduce the number of keystrokes it takes to launch a console:

namespace :fly do
  task :ssh do
    sh 'fly ssh console --pty -C "sudo -iu rails"'
  end

  task :console do
    sh 'fly ssh console --pty -C "/rails/bin/rails console"'
  end

  task :dbconsole do
    sh 'fly ssh console --pty -C "/rails/bin/rails dbconsole"'
  end
end

You can run these tasks with bin/rails fly:ssh, bin/rails fly:console, and bin/rails fly:dbconsole respectively.


Sidekiq Background Workers

Rails applications commonly defer complex tasks that take a long to complete to a background worker to make web responses seem fast. This guide shows how to use Sidekiq, a popular open-source Rails background job framework, to set up background workers, but it could be done with other great libraries like Good Job, Resque, etc.

Provision a Redis server

Sidekiq depends on Redis to communicate between the Rails server process and the background workers. Follow the Redis setup guide to provision a Redis server and set a REDIS_URL within the Rails app. Be sure to set the REDIS_URL via a secret as demonstrated here.

Verify the REDIS_URL is available to your Rails application before you continue by running:

fly ssh console -C "printenv REDIS_URL"
REDIS_URL=redis://default:yoursecretpassword@my-apps-redis-host.internal:6379

If you don’t see REDIS_URL in the command above, Sidekiq won’t be able to connect and process background jobs.

Run multiple processes

Most production Rails applications run background workers in a separate process. There’s a few ways of accomplishing that on Fly that are outlined in the multiple-processes docs.

The quickest way to run multiple processes in one region is via the processes directive in the fly.toml file.

The [processes] directive currently only works within a single Fly region. Scaling a Rails application to multiple regions requires a different approach to running multiple processes.

Add the following to the fly.toml:

[processes]
app = "bin/rails server"
worker = "bundle exec sidekiq"

Then under the [http_service] directive, add processes = ["app"]. The configuration file should look something like this:

[http_service]
  processes = ["app"] # this service only applies to the app process
  internal_port = 3000
  force_https = true
  auto_stop_machines = true
  auto_start_machines = true
  min_machines_running = 0

This associates the process with the service that Fly launches.

Deploy and test

Once multiple processes are configured in the fly.toml file, deploy them via:

fly deploy

If all goes well the application should launch with both app and worker processes. Be sure to run through the application and test features that kick-off background jobs. If you’re having issues getting it working, run fly logs to see errors.

Scaling

Scaling up and down processes may be accomplished by running:

fly scale count app=3 worker=3

To view the current state of the application’s scale, run:

fly status
App
  Name     = my-rails-app
  Owner    = personal
  Version  = 41
  Status   = running
  Hostname = my-rails-app.fly.dev

Instances
ID        PROCESS VERSION REGION  DESIRED STATUS  HEALTH CHECKS       RESTARTS  CREATED
15088508  worker  41      ord     run     running                     0         34s ago
8789ef49  app     41      ord     run     running 1 total, 1 passing  0         2022-07-26T16:06:34Z
c419942b  app     41      ord     run     running 1 total, 1 passing  0         2022-07-26T16:05:52Z
ea7af986  app     41      ord     run     running 1 total, 1 passing  0         2022-07-26T16:05:52Z
d681c33d  worker  41      ord     run     running                     0         2022-07-26T15:42:30Z
d8d8dc08  worker  41      ord     run     running                     0         2022-07-26T15:42:30Z

In this case, we can see that 3 worker processes and 3 app processes are running in the ord region.


Environment Configuration

Rails applications are usually configured via the encrypted credentials.yml file or via environmental variables.

Environmental variables

Environment variables are a great way to configure a Rails application that needs to run in multiple environments.

Secret variables

Environment variables that have sensitive data in them, like a DATABASE_URL that contains a password, can be kept in a secret that can’t be viewed except when the container is running.

To set a secret in Fly, run:

fly secrets set SUPER_SECRET_KEY=password1234

Non-sensitive variables

Variables that don’t have sensitive data can be set in the fly.toml file under the [env] directive. An example file might look like:

[env]
  RAILS_LOG_TO_STDOUT = x

View the variables

To view the environment variables of your Fly Rails app, run:

fly ssh console -C "printenv"

There you’ll see all of the environment variables in your application that have been set by fly secrets, the [env] directive in the fly.toml file, and the environment directive from your Dockerfile.

Encrypted credentials file

Another approach to managing credentials in Rails is to use an encrypted credentials file, such as config/credentials.yml.enc or config/credentials/production.yml.enc, which you can learn more about by running the following from the root of your Rails application:

bin/rails credentials:help

When deploying to production, the RAILS_MASTER_KEY that will decrypt the credentials file can be set via fly secrets set. For example, if your master key is stored in config/master.key, you can run:

fly secrets set RAILS_MASTER_KEY=$(cat config/master.key)

For Windows/PowerShell:

$Env:RAILS_MASTER_KEY = Get-Content 'config\master.key'

Most of the documentation from the sidebar, but all on one big page so you can search your way to victory via Ctrl + F or Cmd + F.

Advanced guides

As your Rails application becomes more popular, it will inevitably grow in complexity, the number of people using it will increase, and pretty much everything else goes up and to the right. Fortunately Fly can reduce some of that complexity by making global deployments easy and provide some best practices for scaling a Rails application on Fly.


Phusion Passenger

This guide shows you how to replace the Puma web server which runs your Rails application with nginx and Phusion Passenger. This may be useful if you need functionality that nginx and/or Passenger provide, such as reverse proxying or hosting multiple applications.

Replacing the Dockerfile

When you previously ran fly launch you were provided with a Dockerfile that was used to package your application. This Dockerfile provided reasonable defaults. One of those defaults was to choose the Puma web server. This is the same default that Rails provides for new applications.

This Dockerfile can be customized or replaced to meet your needs. This guide will show you how to replace the Dockerfile with one that chooses Phusion Passenger. The new Dockerfile will make use of the phusion/passenger-full image.

To get started, replace your Dockerfile with the following:

FROM phusion/passenger-full:2.3.0
RUN rm -f /etc/service/nginx/down
RUN rm -f /etc/service/redis/down

RUN rm /etc/nginx/sites-enabled/default
ADD config/fly/rails.conf /etc/nginx/sites-enabled/rails.conf
ADD config/fly/envvars.conf /etc/nginx/main.d/envvars.conf

ENV RAILS_LOG_TO_STDOUT true

ARG BUNDLE_WITHOUT=development:test
ENV BUNDLE_WITHOUT ${BUNDLE_WITHOUT}

RUN mkdir /app
WORKDIR /app
RUN mkdir -p tmp/pids

COPY Gemfile* ./
RUN bundle install

ENV SECRET_KEY_BASE 1

COPY . .

RUN bundle exec rails assets:precompile

CMD ["/sbin/my_init"]

As promised, the passenger-full image that the Phusion team provides makes your Dockerfile considerably smaller, but there remains more work to be done as you still need to configure nginx. We do that next.

Configuring nginx

The Dockerfile above contains three ADD commands. These copy configuration files to the image. The first two files configure ngix. Place all three files in a config/fly directory.

We start with config/fly/rails.conf:

server {
    listen 8080;
    server_name www.webapp.com;
    root /app/public;

    # The following deploys your Ruby/Python/Node.js/Meteor app on Passenger.

    # Not familiar with Passenger, and used (G)Unicorn/Thin/Puma/pure Node before?
    # Yes, this is all you need to deploy on Passenger! All the reverse proxying,
    # socket setup, process management, etc are all taken care automatically for
    # you! Learn more at https://www.phusionpassenger.com/.
    passenger_enabled on;
    passenger_user app;

    # If this is a Ruby app, specify a Ruby version:
    # For Ruby 3.1
    passenger_ruby /usr/bin/ruby3.1;
    # For Ruby 3.0
    # passenger_ruby /usr/bin/ruby3.0;
    # For Ruby 2.7
    # passenger_ruby /usr/bin/ruby2.7;
    # For Ruby 2.6
    # passenger_ruby /usr/bin/ruby2.6;

    # Nginx has a default limit of 1 MB for request bodies, which also applies
    # to file uploads. The following line enables uploads of up to 50 MB:
    client_max_body_size 50M;
}

The servername doesn’t particularly matter, but you can set it to the name of the machine your application will be deployed to.

The only real customization required is to select the version of Ruby desired.

Next we need to identify what environment variables will be used by the application. See the passenger documentation for more details. We do that by placing the following into config/fly/envvars.conf:

env DATABASE_URL;
env REDIS_URL;
env RAILS_LOG_TO_STDOUT;

The above are commonly used variables, feel free to adjust as you see fit.

Enabling swap

See swap_size_mb for configuring for deploys.

Deployment

That’s it. As always you deploy your application via fly deploy and can open it via fly apps open. Everything else remains the same. You can use your same Postgre database, redis data store, and any other secrets you may have set.

Both Puma and Passenger are excellent choices for application servers for your Rails application, so you normally wouldn’t have a need to replace one with the other. Further configuration likely is required to unlock specific features of nginx or passenger for your particular needs. The above is only intended as an initial configuration to get you started. You have full control over what is installed on your machine and how both nginx and passenger are configured.

The sky’s the limit on what you can achieve with these instructions!


Multiple Fly Applications

This guide discusses how to manage multiple Fly applications within a Rails projects. This is useful for Rails projects that need to run other services, like running a pool of Puppeteer servers that your Rails app calls to take screenshots of web pages.

What is a Fly application?

Your Rails application is a Fly application, which means it has the following few things:

  • A fly.toml file in the root directory
  • A Dockerfile in the root directory that describes the image
  • A “server” running on Fly’s infrastructure.

So how do you spin up multiple Fly applications for a single Rails project?

Creating a Fly application within a Fly application

The important thing about creating multiple Fly applications within a project is keeping them organized. For our example, we’ll setup a Redis server application and keep it in the ./fly/applications folder within our project repo.

Let’s get started by running the following commands:

mkdir -p fly/applications/redis
cd fly/applications/redis

From inside the fly/applications/redis folder, run:

fly launch --image flyio/redis:6.2.6 --no-deploy --name my-project-name-redis

This command will create a Dockerfile and fly.toml file that can be further configured for your application’s needs.

Next, deploy the application:

fly deploy

Accessing from the root application

Fly creates DNS hosts for each of your applications that are not surprising.

Deploying updates

In the future, when it’s time to deploy updates to your Fly application within a Fly application, run:

cd fly/application/redis
fly deploy

That’s it.


Machine API

This is a technology preview. It demonstrates how you can launch fly machines dynamically to perform background tasks from within a Rails application.

Deploying a Rails project as a Fly.io Machine

rails new welcome; cd welcome

Now use your favorite editor to make a one line change to config/routes.rb:

 Rails.application.routes.draw do
   # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html

   # Defines the root path route ("/")
-  # root "articles#index"
+  root "rails/welcome#index"
 end

Install fly.io-rails gem:

 bundle add fly.io-rails

Source to this gem is on GitHub. If adopted, it will move to the superfly organization.

Optionally configure your project:

bin/rails generate fly:app --passenger --serverless

The above command will configure you application to scale to zero whenever it has been idle for 5 minutes. See generator options for more details.

Feel free to tailor the generated files further to suit your needs. If you don’t run the fly:app generator, the files necessary to deploy your application will be generated on your first deploy using default options.

Deploy your project

bin/rails deploy

You have now successfully deployed a trivial Rails app Fly.io machines platform.

You can verify that this is running on the machines platform via fly status. You can also run commands like fly apps open to bring your application up in the browser.

Now lets make that application launch more machines.

Installing fly on the Rails Machine

Since we will be using fly services from within our Rails application, we will need to install the fly executable. We do that by adding the following lines to our Dockerfile:

RUN curl -L https://fly.io/install.sh | sh
ENV FLYCTL_INSTALL="/root/.fly"
ENV PATH="$FLYCTL_INSTALL/bin:$PATH"

A good place to put these lines is immediately before the # Deploy your application comment.

Next we need to make our Fly token available to our application:

fly secrets set FLY_API_TOKEN=$(fly auth token)

Add a controller

Lets generate a controller with three actions:

bin/rails generate controller job start complete status

The three actions will be as follows:

  • job/start will be the URL you will load that will kick off a job.
  • job/complete will be the URL that job will fetch once it is complete.
  • job/status will be the URL you will load once the job is complete to see the results.

To keep things simple, all we are going to do is have these tasks write timestamps to a file, one when the job starts, and one when the job completes. Status will return the results of the file.

The code to do this is straightforward:

class JobController < ApplicationController
  skip_before_action :verify_authenticity_token

  def start
    File.write 'tmp/status', `date +"%d-%m-%Y %T.%N %Z"`
    url = "http://#{request.host_with_port}/job/complete"
    job = MachineJob.perform_later(url)
    render plain: "#{job}\n", layout: false
  end

  def complete
    File.write 'tmp/status', `date +"%d-%m-%Y %T.%N %Z"`, mode: 'a+'
    render plain: "OK\n", layout: false
  end

  def status
    render plain: IO.read('tmp/status'), layout: false
  end
end

Note that the start method provides the complete URL of the complete action as a parameter to the machine job.

Before moving on, lets make sure that the file exists:

touch tmp/status

Add a Job

We start by generating a job:

bin/rails generate job machine

Overall the tasks to be performed by this job:

  • Specify a machine configuration. For simplicity we will use the same Fly application name and the same Fly image as our Rails application. The server command will be curl specifying the URL that was passed as an argument to the job.
  • Start a machine using this configuration, and check for errors, and log the results.
  • Query the status of the machine every 10 seconds for a maximum of 5 minutes, checking to see if the machine has exited.
  • Extract the exit code and log the state. If the machine has exited successfully we delete the machine, otherwise we leave it around so that further forensics can be performed.

The implementation of this plan is as follows:

require 'fly.io-rails/machines'

class MachineJob < ApplicationJob
  queue_as :default

  def perform(url)
    if Rails.env.production?
      # specify a machine configuration
      app = ENV['FLY_APP_NAME']

      config = {
        image: ENV['FLY_IMAGE_REF'],
        guest: {cpus: 1, memory_mb: 256, cpu_kind: "shared"},
        env: {'SERVER_COMMAND' => "curl #{url}"}
      }

      # start a machine
      start = Fly::Machines.create_and_start_machine(app, config: config)
      machine = start[:id]

      if machine
        logger.info "Started machine: #{machine}"
      else
        logger.error 'Error starting job machine'
        logger.error JSON.pretty_generate(start)
        return
      end

      # wait for machine to complete, checking every 10 seconds,
      # and timing out after 5 minutes.
      event = nil
      30.times do
        sleep 10
        status = Fly::Machines.get_a_machine app, machine
        event = status[:events]&.first
        break if event && event[:type] == 'exit'
      end

      # extract exit code
      exit_code = event.dig(:request, :exit_event, :exit_code)

      if exit_code == 0
        # delete job machine
        delete = Fly::Machines.delete_machine app, machine
        if delete[:error]
          logger.error "Error deleting machine: #{machine}"
          logger.error JSON.pretty_generate(delete)
        else
          logger.info "Deleted machine: #{machine}"
        end
      else
        logger.error 'Error performing job'
        logger.error (exit_code ? {exit_code: exit_code} : event).inspect
      end
    else
      system "curl", url
    end
  end
end

Note in particular the calls to Fly::Machines:

Each of the lines in the list above is a link to the documentation for that API.

The key difference is that instead of passing in and receiving back a JSON object, you pass in and receive back a Ruby hash. And all of the URLs and HTTP headers are taken care of for you by the Fly::Machine module.

For those interested in the inner workings, the source to Fly::Machine is on GitHub. Again, all this is beta, and subject to change.

Trying it out

We are now ready to deploy, but before we do in a separate window start watching the log::

fly logs

Now deploy the application:

bin/rails deploy

If you run fly apps open you will arrive at your application’s welcome page. Take a note of the URL. Either in the browser or in a command window add /job/start. As a response (either in your browser or terminal window you will see something like:

#<MachineJob:0x00007f2b31b047e0>

Switching to your log window you should see output similar to the following:

2022-09-20T04:07:59Z app[9080514c12d787] iad [info]I, [2022-09-20T04:07:59.525790 #514]  INFO -- : [b2f7eb1f-e552-445d-84ac-72d4a63fa4d4] Started GET "/job/start" for 168.220.92.2 at 2022-09-20 04:07:59 +0000
2022-09-20T04:07:59Z app[9080514c12d787] iad [info]I, [2022-09-20T04:07:59.529213 #514]  INFO -- : [b2f7eb1f-e552-445d-84ac-72d4a63fa4d4] Processing by JobController#start as HTML
2022-09-20T04:07:59Z app[9080514c12d787] iad [info]I, [2022-09-20T04:07:59.541820 #514]  INFO -- : [b2f7eb1f-e552-445d-84ac-72d4a63fa4d4] [ActiveJob] Enqueued MachineJob (Job ID: 7984003d-bb82-4815-acff-81d1ba91539f) to Async(default) with arguments: "http://weathered-sunset-3812.fly.dev/job/complete"
2022-09-20T04:07:59Z app[9080514c12d787] iad [info]I, [2022-09-20T04:07:59.544837 #514]  INFO -- : [b2f7eb1f-e552-445d-84ac-72d4a63fa4d4]   Rendered text template (Duration: 0.0ms | Allocations: 8)
2022-09-20T04:07:59Z app[9080514c12d787] iad [info]I, [2022-09-20T04:07:59.545257 #514]  INFO -- : [b2f7eb1f-e552-445d-84ac-72d4a63fa4d4] Completed 200 OK in 16ms (Views: 2.9ms | Allocations: 1056)
2022-09-20T04:07:59Z app[9080514c12d787] iad [info]I, [2022-09-20T04:07:59.546683 #514]  INFO -- : [ActiveJob] [MachineJob] [7984003d-bb82-4815-acff-81d1ba91539f] Performing MachineJob (Job ID: 7984003d-bb82-4815-acff-81d1ba91539f) from Async(default) enqueued at 2022-09-20T04:07:59Z with arguments: "http://weathered-sunset-3812.fly.dev/job/complete"
2022-09-20T04:07:59Z app[9080514c12d787] iad [info]E, [2022-09-20T04:07:59.546905 #514] ERROR -- : [ActiveJob] [MachineJob] [7984003d-bb82-4815-acff-81d1ba91539f] danger
2022-09-20T04:07:59Z runner[5683009c17548e] iad [info]Reserved resources for machine '5683009c17548e'
2022-09-20T04:07:59Z runner[5683009c17548e] iad [info]Pulling container image
2022-09-20T04:08:00Z app[9080514c12d787] iad [info]I, [2022-09-20T04:08:00.071564 #514]  INFO -- : [ActiveJob] [MachineJob] [7984003d-bb82-4815-acff-81d1ba91539f] Started machine: 5683009c17548e
2022-09-20T04:08:00Z runner[5683009c17548e] iad [info]Unpacking image
2022-09-20T04:08:02Z runner[5683009c17548e] iad [info]Configuring firecracker
2022-09-20T04:08:03Z app[5683009c17548e] iad [info]  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
  0     0    0     0    0     0  load  Upload   Total   Spent    Left  Speed
2022-09-20T04:08:04Z app[9080514c12d787] iad [info]I, [2022-09-20T04:08:04.813284 #514]  INFO -- : [c6131d3b-bcba-4ab7-88cb-3e07800ec6b2] Started GET "/job/complete" for 2a09:8280:1::7635 at 2022-09-20 04:08:04 +0000
2022-09-20T04:08:04Z app[9080514c12d787] iad [info]I, [2022-09-20T04:08:04.814303 #514]  INFO -- : [c6131d3b-bcba-4ab7-88cb-3e07800ec6b2] Processing by JobController#complete as */*
2022-09-20T04:08:04Z app[9080514c12d787] iad [info]I, [2022-09-20T04:08:04.826253 #514]  INFO -- : [c6131d3b-bcba-4ab7-88cb-3e07800ec6b2]   Rendered text template (Duration: 0.0ms | Allocations: 2)
2022-09-20T04:08:04Z app[9080514c12d787] iad [info]I, [2022-09-20T04:08:04.827434 #514]  INFO -- : [c6131d3b-bcba-4ab7-88cb-3e07800ec6b2] Completed 200 OK in 13ms (Views: 2.1ms | Allocations: 167)
100     3    0     3    0     0      1      0 --:--:--  0:00:02 --:--:--     2
2022-09-20T04:08:04Z app[5683009c17548e] iad [info]OK
2022-09-20T04:08:07Z runner[5683009c17548e] iad [info]machine exited with exit code 0, not restarting
2022-09-20T04:08:10Z app[9080514c12d787] iad [info]I, [2022-09-20T04:08:10.238155 #514]  INFO -- : [ActiveJob] [MachineJob] [7984003d-bb82-4815-acff-81d1ba91539f] Deleted machine: 5683009c17548e
2022-09-20T04:08:10Z app[9080514c12d787] iad [info]I, [2022-09-20T04:08:10.238536 #514]  INFO -- : [ActiveJob] [MachineJob] [7984003d-bb82-4815-acff-81d1ba91539f] Performed MachineJob (Job ID: 7984003d-bb82-4815-acff-81d1ba91539f) from Async(default) in 10691.24ms

And, finally, visit /job/status to see the results. Durations will vary, but I’m currently seeing a total elapsed time of anywhere from about two and a half seconds to five seconds.

Recap

While not exactly a realistic application, this application demonstrates a number of important features. Scheduling a job. Launching, monitoring, and removing a machine. Inter-machine communications.

With the right parameters, you can start machines in remote geographic locations or with volumes attached. These machines will have access to your application’s secrets so they can access databases or other cloud services. And if you go back and look at the main.tf file in your application directory you can get an indication of what steps are required, and what options are required for each step, to launch a Rails application.

The possibilities are unlimited.

At the moment, this is only a preview, so API names and options may change. But do try this out, and if you have questions or come up with an exciting usage of this, let us know on community.fly.io.


SQLite3

While Rails applications on Fly.io normally run on Postgres databases, you can choose to run them on sqlite3.

To make this work, you will need to place your databases on persistent Volumes as your deployment image will get overwritten the next time you deploy.

Volumes are limited to one host, this currently means that fly.io hosted Rails applications that use sqlite3 for their database can’t be deployed to multiple regions.

But if you are okay using beta software, LiteFS could work for multi-region sync, check it out! But this guide assumes you have one node and one volume.

Following are the steps required to make this work:

Create volume

fly volumes create name

Replace name with your desired volume name. Only alphanumeric characters and underscores are allowed in names.

Optionally, you may specify the size of the volume, in gigabytes, by adding a --size int argument. The default volume size is 3 gigabytes.

Now set the following secret, again replacing the name with what you selected:

fly secrets set DATABASE_URL=sqlite3:///mnt/name/production.sqlite

Mount and Prep for Deployment

Add the following to your fly.toml, once again replacing the name with what you selected, this time in two places:

[mounts]
  source="name"
  destination="/mnt/name"

Next move the dependency on the db:migrate task from the release to server in lib/tasks/fly.rake:

 # commands used to deploy a Rails application
 namespace :fly do
   # BUILD step:
   #  - changes to the filesystem made here DO get deployed
   #  - NO access to secrets, volumes, databases
   #  - Failures here prevent deployment
   task :build => 'assets:precompile'

   # RELEASE step:
   #  - changes to the filesystem made here are DISCARDED
   #  - full access to secrets, databases
   #  - failures here prevent deployment
-  task :release => 'db:migrate'
+  task :release

   # SERVER step:
   #  - changes to the filesystem made here are deployed
   #  - full access to secrets, databases
   #  - failures here result in VM being stated, shutdown, and rolled back
   #    to last successful deploy (if any).
-  task :server do
+  task :server => 'db:migrate' do
     sh 'bin/rails server'
   end
 end

You can also silence warnings about running sqlite3 in production by adding the following line to config/environments/production.rb:

 Rails.application.configure do
+  config.active_record.sqlite3_production_warning=false

Deploy

These changes can be deployed using fly deploy.


LiteFS

This is a technology preview. It shows how to do multi-region deployments using Sqlite3 and Litefs. See LiteFS - Distributed SQLite for background.

In order to run this demo, you need flyctl to be version 0.1.9 or later.

Deploying a Rails project as a Fly.io Machine

Let’s start with a very simple Rails project:

rails new list
cd list
bin/rails generate scaffold Name name
echo 'Rails.application.routes.draw {root "names#index"}' >> config/routes.rb

Launching this will require a litefs configuration file (litefs.yml) and a number of changes to your dockerfile. Fly.io provides a dockerfile generator which will do this for you. Run it immediately after fly launch thus:

fly launch
bin/rails generate dockerfile --litefs

fly launch will prompt you for a name, region, and whether or not you want postgres or redis databases. Say no to the databases, you won’t need them for this demo.

generate dockerfile will prompt you whether or not you want to accept the changes. Feel free to peruse the diffs, but ultimately accept the changes. If you would rather not even be prompted to see the diffs, you can add a --force option the command.

Before we deploy, let’s make a one-line change to our fly.toml to keep our machines running so that we can ssh into them whenever we want:

  auto_stop_machines = false

Now we can deploy normally:

fly deploy

Once the application has been deployed, running fly apps open will open a browser. Add one name.

Return back to your terminal window and run:

fly machines list -q

You will see that only one copy of your application is running. You can deploy a second machine in a different region using:

fly machine clone --region lhr 3d8d9930b32189

Feel free to pick a different region. Substitute the machine id with the one that you see in the response to fly machines list -q. Once both instances are running, enter the following command and select the instance that is furthest from you:

% fly ssh console -s
? Select VM:  [Use arrows to move, type to filter]
  atl: 3d8d9930b32189 fdaa:0:d445:a7b:e5:b340:6b3d:2 autumn-breeze-5346
> lhr: e784e90ea17928 fdaa:0:d445:a7b:13e:8621:f8bd:2 muddy-moon-3291

Once you see a prompt, verify that you landed where you expected:

printenv FLY_REGION

Now run rails console and display the last name:

% /rails/bin/rails console
Loading production environment (Rails 7.0.4)
irb(main):001:0> Name.last

Return to the browser and change the value of this name, and then once again use the rails console to verify that the name has been updated.

Current limitations


Multi-region Deployments

Rails applications that are successful often end up with users from all parts of the world. Learn how to make them fast and responsive for everybody on the planet.

The fly-ruby gem

The fly-ruby gem provides a basic toolkit for making Rails applications across multiple regions. Read Run Ordinary Rails Apps Globally to see how the gem can be used in a Rails application to make it work across multiple regions.

Redis

Since many Rails applications depend on Redis for caching, background workers, and Action Cable, it’s important to think through deploying Redis globally.

Postgres

Postgres also requires consideration for global deployments.


AnyCable

This guide shows you how to replace Action Cable with AnyCable. Both provide similar functionality, but have different scaling characteristics. While AnyCable has the potential to reduce RAM requirements on large deployments, the need to run multiple processes makes it impossible to run AnyCable alongside even a tiny Rails application on a 256MB machine. A minimum of 512MB is required.

The configuration below also runs nginx as a reverse proxy to avoid any firewall issues.

It also runs everything on one VM, which has a number of downsides including dropping socket connections every time a new version of your application is deployed. For another take on configuring AnyCable to run on fly.io, see Fly.io Deployment on the anycable site.

Prepare your application

If you haven’t already dones so, perform the steps described in the Provisioning Redis section of the Getting Started guide. Also be sure that the pg gem is listed in your Gemfile.

Now add the anycable-rails gem:

bundle add anycable-rails

Edit config/cable.yml thus:

 production:
-  adapter: redis
-  url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %>
-  channel_prefix: namelist_production
+  adapter: any_cable

Update your image

Modify Dockerfile to install foreman, anycable-go and nginx:

 FROM base

+RUN gem install foreman
+RUN curl -L https://go.dev/dl/go1.19.linux-amd64.tar.gz | tar xz -C /opt
+RUN GOBIN=/usr/local/bin /opt/go/bin/go install github.com/anycable/anycable-go/cmd/anycable-go@latest
+
-ARG PROD_PACKAGES="postgresql-client file vim curl gzip libsqlite3-0"
+ARG PROD_PACKAGES="postgresql-client file vim curl gzip libsqlite3-0 nginx"
 ENV PROD_PACKAGES=${PROD_PACKAGES}

 RUN --mount=type=cache,id=prod-apt-cache,sharing=locked,target=/var/cache/apt \
     --mount=type=cache,id=prod-apt-lib,sharing=locked,target=/var/lib/apt \
     apt-get update -qq && \
     apt-get install --no-install-recommends -y \
     ${PROD_PACKAGES} \
     && rm -rf /var/lib/apt/lists /var/cache/apt/archives
+
+ADD config/nginx.conf /etc/nginx/sites-available/default

 COPY --from=gems /app /app
 COPY --from=node_modules /app/node_modules /app/node_modules

Edit fly.toml to run foreman::

 [env]
   PORT = "8080"
-  SERVER_COMMAND = "bin/rails fly:server"
+  SERVER_COMMAND = "foreman start -f Procfile.fly"

Add Procfile.fly to start the four processes required:

nginx: nginx -g 'daemon off;'
server: bin/rails server -p 8081
anycable: bundle exec anycable
ws: anycable-go --port=8082

Add config/nginx.conf to reverse proxy cable traffic to anycable-go and send the remainder to your Rails application::

server {
        listen 8080 default_server;
        listen [::]:8080 default_server;

        location /cable {
                proxy_pass http://localhost:8082/cable;
                proxy_http_version 1.1;
                proxy_set_header Upgrade $http_upgrade;
                proxy_set_header Connection "Upgrade";
                proxy_set_header Host $host;
        }

        location / {
                proxy_pass http://localhost:8081/;
                proxy_set_header origin 'https://localhost:8081';
        }
}

Deployment

If you haven’t already done so, scale your machine:

fly scale vm shared-cpu-1x --vm-memory 512

Now you are ready to deploy:

fly deploy