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.
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 installed, and then by creating a new application:
$ rails --version
$ 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
# Reveal health status on /up that returns 200 if the app boots
# with no exceptions, otherwise 500.
# Can be used by load balancers and uptime monitors to verify
# that the app is live.
get "up" => "rails/health#show", as: :rails_health_check
# 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.
Launch
To configure and launch the app, you can use fly launch
and follow the
wizard. This demo does not need a database or redis.
fly launch
Scanning source code
Detected a Rails app
Creating app in ~/tmp/welcome
We're about to launch your Rails app on Fly.io. Here's what you're getting:
Organization: Jane Developer (fly launch defaults to the personal org)
Name: welcome-proud-sun-3423 (generated)
Region: Ashburn, Virginia (US) (this is the fastest region for you)
App Machines: shared-cpu-1x, 1GB RAM (most apps need about 1GB of RAM)
Postgres: <none> (not requested)
Redis: <none> (not requested)
Tigris: <none> (not requested)
? Do you want to tweak these settings before proceeding? (y/N)
For demo purposes you can accept the defaults. You can always change these later. So respond with “N” (or simply press enter).
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?
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 one line of code and just one command.
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 aDockerfile
and abin/docker-entrypoint
for you which you are free to modify. - As your application needs change, you can have us update your
Dockerfiles
for you by running:bin/rails generate dockerfile
. - There is also a
config/dockerfile.yml
file which keeps track of your dockerfile generation options. This is covered by the FAQ. - Other files of note:
.dockerignore
andfly.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 usingfly
commands. Tryfly help
to see what you can do.fly ssh console
can be used to ssh into your VM.fly 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 theassets: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:
- Don’t use Disk service unless you put the data on a Volume and are prepared to sync the data between machines
- If you want to have your active storage data hosted on fly.io, consider using the postgres adapter or MinIO.
- Of course, you are welcome to use Amazon S3, Microsoft Azure, or Google Cloud Services.
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/upstash/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.
Active Storage
Fly.io is a great place to deploy Rails applications that make use of Active Storage, and Fly.io knows that Rails users prefer Convention over Configuration and their menus Omakase.
Launching a new Application?
If any of the following are true, fly launch
will take care of all of the configuration:
aws-sdk-s3
inGemfile
orGemfile.lock
- any migration containing
active_storage_attachments
- an uncommented line in
config/storage.yml
containingservice: S3
Don’t want Tigris for any reason? Hey - we don’t judge here. Simply disable the extension in the web UI, and setup active storage for yourself.
Adding Tigris to an existing application?
No problem! It can be as easy as a two step process.
Step 1: Create a storage bucket:
fly storage create
? Choose a name, use the default, or leave blank to generate one:
Your Tigris project (xxx) is ready. See details and next steps with: https://fly.io/docs/tigris/
Setting the following secrets on xxx:
AWS_ACCESS_KEY_ID: tid_xxx
AWS_ENDPOINT_URL_S3: https://fly.storage.tigris.dev
AWS_REGION: auto
AWS_SECRET_ACCESS_KEY: tsec_xxxxx
BUCKET_NAME: xxx
Secrets are staged for the first deployment
Step 2: Let dockerfile-rails
do the configuring for you:
bin/rails generate dockerfile --tigris
Note: you don’t need to accept changes to your Dockerfile
, .dockerignore
,
bin/docker-entrypoint
or other files. The only files that need to be updated are:
config/storage.yml
config/environments/production.rb
Want a demo?
Following is a quick and dirty demo that enables you to upload files containing images, audio, video, and other assorted files for viewing and/or downloading.
First, some scaffolding:
rails new filelist --css tailwind
cd filelist
bin/rails active_storage:install
bin/rails generate scaffold Item name:string contents:attachment
bin/rails db:migrate
Next we need to modify three files to complete the application.
With this in place, you are ready to launch:
fly launch
Watch the app deploy and then upload, view, and download a few files!
Find out more!
Now that you are up and running, there is a lot more to explore on the Tigris Global Object Storage page. Highlights include public buckets, migrating to Tigris with shadow butckets, Pricing, and AWS API compatibility.
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
For Postgresql, migrations are configured to automatically run after each
deployment via the following task in your application’s fly.toml
:
[deploy]
release_command = './bin/rails db:prepare'
Sqlite3 migrations are done by the bin/docker-entrypoint
script.
To disable automatic migrations for deploys, remove db:prepare
lines from these files. 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
ls
Dockerfile README.md config lib test
Gemfile Rakefile config.ru log tmp
Gemfile.lock app db public vendor
Procfile.dev bin fly.toml storage
bundle exec ruby my-hello-world-script.rb
hello world
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.
Active Record
In this guide we’ll develop and deploy a Rails application that scaffolds a database table.
Create an application
Start by verifying that you have Rails installed, and then by creating a new application:
$ rails --version
$ rails new list --database postgresql
$ cd list
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
, and connect the root 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.
Launch
To configure and launch the app, you can use fly launch
and follow the
wizard. Fly.io will automatically detect that Postgres is required by
your application.
fly launch
Detected a Rails app
Creating app in ~/tmp/list
We're about to launch your Rails app on Fly.io. Here's what you're getting:
Organization: Joe Developer (fly launch defaults to the personal org)
Name: list-little-fire-1088 (generated)
Region: Ashburn, Virginia (US) (this is the fastest region for you)
App Machines: shared-cpu-1x, 1GB RAM (most apps need about 1GB of RAM)
Postgres: (Fly Postgres) 1 Node, shared-cpu-1x, 256MB RAM, 1GB disk (determined from app source)
Redis: <none> (not requested)
Tigris: <none> (not requested)
? Do you want to tweak these settings before proceeding? (y/N)
For demo purposes you can accept the defaults. You can always change these later. So respond with “N” (or simply press enter).
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?
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 your 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 a Fly token available to our application:
fly secrets set FLY_API_TOKEN=$(fly tokens deploy)
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
- What Litefs Can Do Today describes current capabilities and future plans.
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