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.)
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.
The resulting Dockerfile says EXPOSE 80. I am supposed to just leave that alone (and let Fly make changes behind the scenes), correct?
Run fly launch
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
#!/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.
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.
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.
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.
# 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?
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.
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.