How to deploy Rails and Sidekiq to Fly.io

This article shows how to deploy a typical Rails application with custom Dockerfile to Fly.io. We’ll deploy Rails and Sidekiq processes as well as PostgreSQL and Redis databases. We’ll also touch on scaling instances or Fly.io specific secrets management.

Flyctl CLI

Fly is an interesting Platform as a service provider who can help you deploy your Rails applications without too much friction.

Most of what we’ll need to do can be accomplish with their command line tool flyctl.

Install flyctl:

$ curl -L https://fly.io/install.sh | sh

And export the required paths:

# Manually add the directory to your $HOME/.bash_profile (or similar)
export FLYCTL_INSTALL="$HOME/.fly"
export PATH="$FLYCTL_INSTALL/bin:$PATH"

Fly account

You can signup for a Fly account directly using the flyctl command line interface:

$ flyctl auth signup
Opening https://fly.io/app/auth/cli/a36796651b11e1bbb06e10d5e17a06df ...

Waiting for session... Done
successfully logged in as (...)

If you have an account, log in instead:

$ flyctl auth login

Note: The Fly free offering won’t be enough for a regular Rails application, so you should either select one of their plans or pay as you go. They don’t accept debit cards, but you can purchase a pre-paid credit.

Secrets management

There are two ways to configure your application enviroment.

If you need to set an environment variable, you can do so in the fly.toml configuration file within the [env] section:

$ cat fly.toml
[env]
  PORT = "8080"
  MY_ENV = "value"
...

But if you need to keep a secret, Fly comes with its own secret management as an alternative to Rails Credentials.

To list current secrets for the application, run fly secrets list:

$ fly secrets list

To set a new secret, run flyctl secrets set:

$ flyctl secrets set MY_SECRET=romance

Rails and PostgreSQL

Fly should be able to detect your Rails application automatically. Run fly launch from your project directory to start:

$ fly launch
pdate available 0.0.395 -> v0.0.425.
Run "flyctl version update" to upgrade.
Creating app in /home/strzibny/rails/template
Scanning source code
Detected a Rails app
? Overwrite "/home/strzibny/rails/template/.dockerignore"? No
? Overwrite "/home/strzibny/rails/template/Dockerfile"? No
? Overwrite "/home/strzibny/rails/template/lib/tasks/fly.rake"? No
? App Name (leave blank to use an auto-generated name): 
Automatically selected personal organization: Josef Strzibny
? Select region: ams (Amsterdam, Netherlands)
Created app cold-wave-2722 in organization personal
Set secrets on cold-wave-2722: RAILS_MASTER_KEY
Wrote config file fly.toml
? Would you like to set up a Postgresql database now? (y/N) 

The above walkthrought will let you use your Dockerfile such as the optimized Fullstaq Ruby Dockerfile coming with Business Class kit.

It will also allow you to create your first database. Choose yes (y):

Creating postgres cluster cold-wave-2722-db in organization personal
Postgres cluster cold-wave-2722-db created
  Username:    postgres
  Password:    dd5d0cddaab6b3cc5f7df64b515cca1fedda3c841ae935a9
  Hostname:    cold-wave-2722-db.internal
  Proxy Port:  5432
  PG Port: 5433
Save your credentials in a secure place -- you won't be able to see them again!
...
Connect to postgres
Any app within the personal organization can connect to postgres using the above credentials and the hostname "cold-wave-2722-db.internal."
For example: postgres://postgres:dd5d0cddaab6b3cc5f7df64b515cca1fedda3c841ae935a9@cold-wave-2722-db.internal:5432
...

These database credentials will be put automatically to your Fly secrets:

Postgres cluster cold-wave-2722-db is now attached to cold-wave-2722
The following secret was added to cold-wave-2722:
  DATABASE_URL=postgres://cold_wave_2722:MpQF57Z6L8Nrw5K@top2.nearest.of.cold-wave-2722-db.internal:5432/cold_wave_2722
Postgres cluster cold-wave-2722-db is now attached to cold-wave-2722

If you used a custom Dockerfile, you might need to specify a Ruby version environment if it differs from .ruby-version. You can do that and other changes within fly.toml file:

$ cat fly.toml
[env]
  PORT = "8080"
  RUBY_VERSION = "3.0.1"
...

You’ll also have to update this file or Fly secrets with any possible environment variables to configure emails or Paddle subscriptions.

After that you can deploy the application with fly deploy:

$ fly deploy
...
--> This release will not be available until the release command succeeds.
   Starting instance
   Configuring virtual machine
   Pulling container image
   Unpacking image
   Preparing kernel init
   Starting init (commit: 249766e)...
   Setting up swapspace version 1, size = 512 MiB (536866816 bytes)
   Preparing to run: `bin/rails fly:release` as template
   2022/10/30 10:15:34 listening on [fdaa:0:cac9:a7b:a27f:9e8d:748b:2]:22 (DNS: [fdaa::3]:53)
   warning: parser/current is loading parser/ruby30, which recognizes3.0.3-compliant syntax, but you are running 3.0.1.
   Please see https://github.com/whitequark/parser#compatibility-with-ruby-mri.
   == 20211211115514 CreateUsers: migrating ======================================
      -> 0.0011s
   I, [2022-10-30T10:15:38.924572 #523]  INFO -- : Migrating to CreateTeamRoles (20220113114759)
   == 20220113114759 CreateTeamRoles: migrating ==================================
   == 20220113175211 CreateInvitations: migrating ================================
...

This task builds a Docker image, pushes it to registry, runs migrations and start your application.

Then open your running application in a browser with fly open:

$ fly open

Console and Rake tasks

To access your Rails console, first request a token and then run flyctl ssh console:

$ flyctl auth token
$ flyctl ssh console

This way you can also run your Rake tasks, such as the initial task to create an admin account in Business Class:

# cd app && rails admin:create[strzibny@gmail.com,password,5]

To run arbitrary commands, have a look at the machine run command.

Redis

Not every application needs Redis, but Business Class comes with a default Active Job and Action Cable setup that needs Redis installed. To add Redis, run:

$ flyctl redis create
...
Your Upstash Redis database sparkling-meadow-6756 is ready.
Apps in the personal org can connect to at redis://default:77bd1a6f2ad04c25ad28e6294eb75389@fly-sparkling-meadow-6756.upstash.io
If you have redis-cli installed, use fly redis connect to connect to your database.
...

After the instance is provisioned, update the environment with the Redis URL that got printed into the terminal:

$ cat fly.toml
[env]
  PORT = "8080"
  RUBY_VERSION="3.0.1"
  REDIS_URL="redis://default:77bd1a6f2ad04c25ad28e6294eb75389@fly-sparkling-meadow-6756.upstash.io"

Sidekiq

If you have successfully finished adding up Redis configuration, you should be able to see that Sidekiq Web console can now be opened. But your workers won’t run at this point unless you configure Sidekiq to run embedded or add at least one worker process.

A regular Sidekiq setup comes with at least one worker process, so let’s change our processes configuration to feature two processes:

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

Then change the app reference to web within the [[services]] block:

$ cat fly.toml 
...
[[services]]
  http_checks = []
  internal_port = 8080
  processes = ["web"]
...

Redeploy the application with fly deploy to see the changes coming alive.

Check that the Sidekiq console now shows more connections than before. That means things are working.

Scaling

If your application doesn’t have enough resources to run or you need more resources to run the Rails console on the server, you can scale the virtual machine with fly scale:

$ fly platform vm-sizes
...
$ fly scale vm dedicated-cpu-1x

Final configuration

This is the final configuration:

$ cat fly.toml 
# fly.toml file generated for cold-wave-2722 on 2022-10-30T11:01:24+01:00

app = "cold-wave-2722"
kill_signal = "SIGINT"
kill_timeout = 5

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

[build]
  [build.args]
    BUILD_COMMAND = "bin/rails fly:build"
    SERVER_COMMAND = "bin/rails fly:server"

[deploy]
  release_command = "bin/rails fly:release"

[env]
  PORT = "8080"
  RUBY_VERSION="3.0.1"
  REDIS_URL="redis://default:77bd1a6f2ad04c25ad28e6294eb75389@fly-sparkling-meadow-6756.upstash.io"

[experimental]
  allowed_public_ports = []
  auto_rollback = true

[[services]]
  http_checks = []
  internal_port = 8080
  processes = ["web"]
  protocol = "tcp"
  script_checks = []
  [services.concurrency]
    hard_limit = 25
    soft_limit = 20
    type = "connections"

  [[services.ports]]
    force_https = true
    handlers = ["http"]
    port = 80

  [[services.ports]]
    handlers = ["tls", "http"]
    port = 443

  [[services.tcp_checks]]
    grace_period = "1s"
    interval = "15s"
    restart_limit = 0
    timeout = "2s"

[[statics]]
  guest_path = "/app/public"
  url_prefix = "/"

Remember that secrets have to be managed separately using flyctl secrets set.

Conclusion

This is one of the most minimal Fly.io setups that won’t cost a fortune, but don’t miss on specific Fly features that let you deploy your applications closer to your users. Check Fly official docs on how to do that.

Tip: if you prefer to understand virtual machines deployment in more details, check out Deployment from Scratch.