Dockerless Rails Development

This is a follow-up to Rails on Fly.io Machines - looking for feedback and Progress update on scaling a Rails Application.

fly launch on a Rails application produces four files: Dockerfile, .dockerignore, fly.toml, and lib/tasks/fly.rake. I’m exploring reducing that to two: fly.toml, and a config/fly.rb. The motivation for this is that the current experience is that the tooling will detect what you need at launch time and get you started, but after that point you are expected to maintain the docker configuration. I want to do better.

Oversimplifying things a bit: fly deploy produces a tar file of your source (minus the files in .dockerignore), and ships both the tar file and the Dockerfile to a builder to produce an image. It then will optionally spin up a machine and run a release command that you specify in fly.toml. Finally if all goes well, it will launch your application.

I want to run the scan that is currently being done at launch time on the build machine, and have it determine what needs to be installed. The problem is that build machines are only set up to take a tar file and a Dockerfile and produce an image. Fortunately, Dockerfiles are Turing complete, and everybody that deploys a Rails application will already be requiring Ruby and other tooling to be installed.

Put together, if fly deploy were invoked from a script (say, bin/rails deploy or even rake deploy), the script could create a Dockerfile and let deploy take it from there. The Dokerfile would probably end up looking something like this. Very, very, generic, the only substitutable parameters would be the version of ruby and bundler.

Not shown in that Dockerfile, but what ultimately will likely happen is that the build will start with installing Ruby, bundler, and the fly.io-rails gem. Inside that gem will be the scanners and scripts necessary to install debian packages, gems, node packages, and update configuration files. All done very much like it is today using build caching when possible, though likely there will be a set of files/directories (e.g., config, Gemfile, package.json) which will cause the build to effectively start over as changes to those files may cause a different set of packages to be installed.

Whenever possible, the configuration will be detected from the existing configuration. When additional configuration is required, that information will either be extracted from fly.toml or config/fly.rb. Those configuration files will contain things like lists of additional pacakges to be installed, or scripts to be run at various points in the process.

From a fly.io platform point of view, these will be vanilla apps, produced by Dockerfiles just like any other app. From a Rails developer point of view, Dockerfiles themselves will fade into the woodwork, and can generally be ignored. In its place will be a series of what amounts to lifecycle hooks that can be written in Ruby, as well as some data (things like lists of packages to be installed).

I don’t have a schedule in mind, but I will post occasional updates as I make progress. Feedback (and patches!) welcome.

3 Likes

Hey @rubys!

Personally, I use my custom Dockerfile and the common fly.toml file.

For that reason, I don’t see the need to run a different rake task just for Fly nor to have an additional gem, unless you want to use the technique of responding with a header to run applications globally.

Also, in my case, I don’t use nodejs, only importmaps, and almost all dockerfiles include nodejs by default. I also include libvips, but others might use imagemagic.

Having my own Dockerfile makes me responsible for updating the Ruby version, which I change from the Dockerfile itself, with no compile arguments or environment variables, but I prefer that to losing control of what’s going on. My Dockerfile is not advanced at all.

So, if Fly wants to have control of some new features, I think a Dockerfile should not be created in the project folder, as any updates to that Dockerfile would be lost. But it’s also not something that is being updated all the time after all.

In my opinion, only the fly.toml file should be created. Just like in Heroku only one Procfile is used.

I think adding gems or more configuration files just creates more confusion for a beginner. For example, why do I have to have a config/fly.rb file?

Maybe another interesting option is to generate the Dockerfile in the folder and make it as simple as possible, without using advanced Docker features, with comments in the relevant parts so the user can change relevant aspects, and a message in the terminal after running the fly launch command to review that file. Also, I think it is very important to say in that message about the fly.toml process block, especially for Rails applications, and give an example.

Also, the most common in Rails applications is to run on port 3000, while Fly encourages using port 8080. That could be another reason for confusion, personally, I still use port 3000.

For example, the Ruby version, the installation of dependencies (libvips or imagemagic which are very common), if it uses nodejs or is an api-only application or uses importmaps, running rake tasks to compile assets (api-only applications don’t need this), etc.

As I go deeper into this idea I am convinced that I think this is the best. Applications can be very different from each other. A Dockerfile is something standard, not something proprietary to Fly, and with good comments you can pave the way for people who know Docker but are just coming to Fly, as well as for people who don’t know Docker and probably don’t need to make changes to the file. And if they do need to make changes, the comments should be very good to allow them to add or remove things. After all, it’s almost running install commands, copying files, and running rake tasks. Maybe there could be a commented line with a link to Fly’s documentation where it is explained in more detail.

When I came to Fly, I had tried Render and Digitalocean Ap Platform, and in all of them I used the same Dockerfile. That allowed me to be able to quickly run my application on Fly without problems, without understanding specific features of the platform. Installing a gem, using the machine api I think are beyond what a simple fly launch command should do. Probably the user who needs to use it doesn’t even understand the difference between v1 and v2 apps yet, and if did understand them, probably wouldn’t be using that command and would be following the steps in the advanced documentation.

For example, this Dockerfile is self-explanatory. If it were an api-only app, you wouldn’t need to run the task to compile the assets. If you were using minimagic, and not libvips, you would have to change the installation in the first part. The same applies if you were using MySQL or SQLite or if you were using Node js.

# See more at ...
# Change Ruby version if needed
FROM quay.io/evl.ms/fullstaq-ruby:3.1.2-jemalloc-bullseye-slim

WORKDIR /app

# Change if you need install imagemagic or mysql or sqlite clients
RUN apt-get update -q \
    && apt-get install --assume-yes -q --no-install-recommends build-essential libpq-dev libvips \
    && apt-get autoremove --assume-yes \
    && rm -Rf /var/cache/apt \
    && rm -Rf /var/lib/apt/lists/*

# Add section to install nodejs. Importmap or api-only applications don't require it

ENV RAILS_ENV production
ENV RAILS_SERVE_STATIC_FILES true
ENV RAILS_LOG_TO_STDOUT true

COPY Gemfile /app/
COPY Gemfile.lock /app/
RUN bundle config --global frozen 1
RUN bundle config set --local without 'development test'
RUN bundle install

COPY . /app

# Execute rake tasks
# If you need precompile assets (api-only applications don't need it)
RUN SECRET_KEY_BASE=dumb bundle exec rake DATABASE_URL=postgresql:does_not_exist assets:precompile

EXPOSE 3000

CMD ["bundle", "exec", "rails", "server", "-b", "0.0.0.0", "-p", "3000"]

I hope I have helped

Hey @brunoprietog !

You are welcome, and even encouraged, to bring your own Dockerfile. I wish more people would do so.

I also don’t use nodejs, only importmaps, but many do. When I started here, the Dockerfile produced by fly launch was more complicated than it is today, and I’ve been simplifying it. And tailoring it so that items were removed if, for example, your app doesn’t use nodejs.

It looks like you use postgresql. I use sqlite3. It turns out a number of changes are required to make that work. Likewise for web sockets.

What I’m finding is that the first half of that statement is true, but the second half (starting with “as well as”)… not so much.

Let’s walk through a concrete example. Suppose your application could benefit from having a swapfile defined. What changes would be required to make that happen?