Precise steps for deploying Rails 8

What are the precise step for deploying a simple “tutorial-like” Rails 8 app on fly?

Should fly launch just work?

When I tried that, the first thing I noticed is that the internal_port in fly.toml doesn’t match the EXPOSE in the Dockerfile.

So, I set internal_port and EXPOSE to 8080. I also added TARGET_PORT="8080" to the ENV command.

The machine still seems to be repeatedly rebooting. Now the problem appears to be ERROR failed to run error="config file not found: /rails/tmp/litestream.yml"

Is this a bug? Or is there part of the bigger picture I’m missing?

(For context, I’m teaching a Web Applications college course. My knowledge of Docker and dev-ops in general is minimal. My students have never done dev ops before, and need a foolproof step-by-step guide.)

fly launch should indeed just work. Here is a precise set of steps, mimicking chapter 6 of Agile Web Development with Rails 8:

rails new depot --css tailwind

cd depot

bin/rails generate scaffold Product \
  title:string description:text image:attachment price:decimal

bin/rails active_storage:install

bin/rails db:migrate

echo 'Rails.application.routes.draw { root "rails/welcome#index" }' >> config/routes.rb

At this point, check the Dockerfile. If it specifies 3.4.2, either change that to 3.4.1, or run the following command:

bin/rails generate dockerfile

Now run fly launch.

There are a lot of stories behind what goes into making this work. A few examples:

  • The Ruby 3.4.2 problem is due to a change in the way the Ruby docker images are being produced. Rails has an unreleased fix for this, I’ve included the fix in our docker file generator.
  • The Rails docker image includes running thruster on port 80 under a non-root user. That’s fine when you are running in a container, but when you are running in a real VM, port 80 is reserved for root. So that is mapped to port 8080. And there are timing issues when restarting machines with thruster that don’t appear when you run kamal, and much what thruster does is unnecessary with fly.io.
  • Your sqlite3 database is placed on a volume, and continuously backed up using litestream. The configuration file for litestream is set up using lib/tasks/litestream.rake.

Excerpt from a working fly.toml:

[env]
  DATABASE_URL = 'sqlite3:///data/production.sqlite3'
  PORT = '8080'

[processes]
  app = './bin/rake litestream:run ./bin/rails server'

[[mounts]]
  source = 'data'
  destination = '/data'
  auto_extend_size_threshold = 80
  auto_extend_size_increment = '1GB'
  auto_extend_size_limit = '10GB'

[http_service]
  internal_port = 8080
  force_https = true
  auto_stop_machines = 'stop'
  auto_start_machines = true
  min_machines_running = 0
  processes = ['app']

I decided to start the deploy from scratch, so I

  1. deleted Dockerfile, .dockerignore, and fly.toml.
  2. ran bin/rails generate dockerfile

The resulting Dockerfile says EXPOSE 80. I am supposed to just leave that alone (and let Fly make changes behind the scenes), correct?

  1. Run fly launch
  2. Accept the default settings.

This time fly.toml does list 8080.

But, the deploy still has errors:

No machines in group app, launching a new machine
WARN failed to release lease for machine 48e2255a43d0e8 [app]: lease not found

-------
 ✖ Failed: timeout reached waiting for health checks to pass for machine 48e2255a43d0e8: failed to get VM 48e2255a43d0e8: Get "https://api.…
-------
Error: timeout reached waiting for health checks to pass for machine 48e2255a43d0e8: failed to get VM 48e2255a43d0e8: Get "https://api.machines.dev/v1/apps/katzmann835/machines/48e2255a43d0e8": net/http: request canceled

When I try to start the machine manually and look at the logs, I see

2025-02-20T19:07:59.572 app[48e2255a43d0e8] ord [info] INFO Starting init (commit: 67f51b8b)...

2025-02-20T19:07:59.725 app[48e2255a43d0e8] ord [info] INFO Preparing to run: `/rails/bin/docker-entrypoint ./bin/rake litestream:run ./bin/rails server` as 1000

2025-02-20T19:07:59.734 app[48e2255a43d0e8] ord [info] INFO [fly api proxy] listening at /.fly/api

2025-02-20T19:08:00.012 runner[48e2255a43d0e8] ord [info] Machine started in 1.41s

2025-02-20T19:08:00.090 app[48e2255a43d0e8] ord [info] 2025/02/20 19:08:00 INFO SSH listening listen_address=[fdaa:d:9413:a7b:316:93b1:40ca:2]:22

2025-02-20T19:08:02.211 app[48e2255a43d0e8] ord [info] 2025/02/20 19:08:02 ERROR failed to run error="config file not found: /rails/tmp/litestream.yml"

2025-02-20T19:08:02.736 app[48e2255a43d0e8] ord [info] INFO Main child exited normally with code: 1

I do have lib/tasks/litestream.rake, but not tmp/litestream.yml.

Should I have to build that locally first? Or should the deploy process be creating this file?

Your bin/dockerfile-entrypoint should have the following:

# If running the rails server then create or migrate existing database
if [ "${@: -2:1}" == "./bin/rails" ] && [ "${@: -1:1}" == "server" ]; then
  ./bin/rails db:prepare
fi

Your fly.toml should have:

[processes]
  app = './bin/rake litestream:run ./bin/rails server'

Note that this ends with ./bin/rails, and server. So db:prepare should be run.

Your lib/tasks/litestream.rb should contain:

namespace :db do
  task prepare: "litestream:prepare"
end

This makes litestream:prepare a dependency of the db:prepare task. litestream:prepare will create tmp/litestream.yml.

You asked for precise steps. I provided some. Did they work for you?

My bin/docker-entrypoint is completely different:


#!/bin/bash -e

# Enable jemalloc for reduced memory usage and latency.
if [ -z "${LD_PRELOAD+x}" ]; then
    LD_PRELOAD=$(find /usr/lib -name libjemalloc.so.2 -print -quit)
    export LD_PRELOAD
fi

# Add any container initialization steps here

exec "${@}"

Should I have to edit that by hand? Or should the fly launch process be producing a different file?

What database are you intending to use? That bin/docker-entrypoint looks correct for a database like Postgres, but for that you wouldn’t want/need litestream at all. Instead db:prepare would be run as a deploy release command.

In software, there’s no such thing as “precise steps”, as the answer can vary so wildly.

But one good rule with Fly is that if your container works locally, it is highly likely to work on Fly as-is. Can you run it locally?

Either way, can we see your Dockerfile? The jemalloc stuff looks pretty involved, and not necessary for a small runnable app.

This is super-helpful advice, but I’d argue that the first-time user doesn’t need a [processes] block at all. If the app runs locally using the CMD in the Dockerfile, then it will run in Fly automatically (minor tweaks for port numbers etc).

I like that recommendation. If you run bin/rails generate dockerfile --compose you will get a docker-compose.yml file. And you can test it with docker compose up.

1 Like

My understanding was that Fly would “just work” for basic Rails apps (without having to tweak the Dockerfile). If that’s not the case , then I need to be looking at this problem through a different lens.

If I want to use sqlite in production (for simplicity in the classroom context), how do I communicate that to the default fly launch algorithm?

If that CMD contains "./bin/thrust", it will indeed start, but when if you are running with auto_stop_machines = 'stop' (which is the default) then when you machine restarts, the first request it receives after restarting will fail.

1 Like

fly launch should indeed just work, with the notable exception of the current Rails bug. If it doesn’t, I will fix it.

The code for the dockerfile generator is here: GitHub - fly-apps/dockerfile-rails: Provides a Rails generator to produce Dockerfiles and related files.

The code for detecting the database is here: dockerfile-rails/lib/dockerfile-rails/scanner.rb at f48c9f23fc888061dd94f170d37bb6260032dde8 · fly-apps/dockerfile-rails · GitHub

The template for producing the dockerfile-entrypoint is a bit involved, but the code is here: dockerfile-rails/lib/generators/templates/docker-entrypoint.erb at f48c9f23fc888061dd94f170d37bb6260032dde8 · fly-apps/dockerfile-rails · GitHub

Essentially for databases other than sqlite3, there is a prepare command; for sqlite3 the prepare is run by the docker entrypoint; if that is not working for you I want to track down what is different in your environment and fix the generator.

Adding

# If running the rails server then create or migrate existing database

if [ "${@: -2:1}" == "./bin/rails" ] && [ "${@: -1:1}" == "server" ]; then

./bin/rails db:prepare

fi

to docker-entrypoint fixed the issue.

How does fly launch determine what db is being used in production? Does it look in config/database.yml? Or does it look at the presence of the pg gem in Gemfile and assume that config/database.yml is set up to use Postgress in production?

Looks like our message may have crossed in transit. It looks at config/database.yml.

Do you have both postgres included in your Gemfile? Can you share your Gemfile? If I can reproduce the problem you are seeing, I can fix it.

Thanks for sending the links. That’s exactly what’s going on:

Immediately after looking through database.yml the code also looks at the gemfile. Since the Gemfile does include pg, @postgresql gets set to true.

I don’t know why the Gemfile is including pg (it is a student’s code), but I can see how the contradiction between the database.yml and Gemfile could be problematic.

So, I would say the primary blame for the problem is on the student, not on your code.

      database = YAML.load_file("config/database.yml", aliases: true).
        dig("production", "adapter") rescue nil

      if database == "sqlite3"
        @sqlite3 = true
      elsif database == "postgresql"
        @postgresql = true
      elsif (database == "mysql") || (database == "mysql2") || (database == "trilogy")
        @mysql = true
      elsif database == "sqlserver"
        @sqlserver = true
      end

      @sqlite3 = true if @gemfile.include? "sqlite3"
      @postgresql = true if @gemfile.include? "pg"
      @mysql = true if @gemfile.include?("mysql2") || using_trilogy?
      @sqlserver = true if @gemfile.include?("activerecord-sqlserver-adapter")

I disagree. bin/rails generate dockerfile should either produce a workable configuration, or produce a meaningful error message. That student’s code may have been weird, but trust me people attempt to launch even weirder stuff all the time.

In this case the bottom 4 lines should probably have been elsif clauses on the previous checks. I’ll make that change later tonight or early tomorrow and release it.

2 Likes

Give this a try:

bundle remove dockerfile-rails
bundle add dockerfile-rails --github fly-apps/dockerfile-rails --branch pick-only-one-database

You can see the change - note the cleanup of a MySQL Dockerfile.

Thinking through it, if both pg and sqlite3 are present, I’m giving precedence to pg for two reasons:

  • sqlite3 is Rails’s default, the addition of pg was intentional.
  • a common configuration is to use sqlite3 in development and pg in production (certainly more common than the other way around).

Feedback welcome.