Rails Dockerfile futures

First, appologies in advance for a long and rambing post. If you are impatient, skip to the bottom as I have recommendations.


So far I’ve been contributing pull requests for which there is essentially no downsides. I have a few more but I’m rapidly approaching the point where there will be tradeoffs. And I’d like to get advice on how to proceed.

A concrete example: right now as it is set up if you add a gem to a Gemfile the bundle install step in the dockerfile will be rerun. The whole step. It won’t just add the one gem, it will reinstall all gems.

This can be improved by the use of --mount=type=cache. And this problem is not just locallized to gems, but also node modules and debian packages.

There is a clear benefit to using a cache. It also makes the Dockerfiles considerably less readable. I happen to like readable Dockerfiles. But I also like fast builds. But how often are you really changing your Gemfile, package.json, or needing to install a new Debian package.

One approach is to decide it is worth it. That’s what fly is currently doing (not yet for gems, but that’s merely an oversight/todo). Another is to provide a generator with options that allow the developer to identify their preference (with defaults that we decide). A third is to document how to make these changes.

There are more decisions like these. I’ve recently added multi-stage builds to the Dockerfile in order to keep the image small. They can also be used to do things like yarn install and bundle install in parallel. Similar tradeoff: small clean Dockerfiles that are fast most of the time vs larger Dockerfiles that are faster when dependencies change.

Dockerfile generation is only doing half of the job needed to deploy Rails API-only servers.

… I could go on, but I hope I’ve made my point.


There are other ramifications that aren’t quite so broad. As an example, per the docs a syntax directive is required in order to support --mount=type=cache or here-docs. But is it required? Another page lists an advantage of syntax directives that it will “Make sure all users are using the same implementation to build your Dockerfile”. And continues to say “We recommend using docker/dockerfile:1”. So in other words, it may work or may not work without that directive. And finally, the best practices page includes the directive. But doesn’t explicitly reiterate the recommendation.

And all for a feature that we have yet to determine how to handle in Rails.


I mentioned that am closing this with a recommendation. First the easy one. I’d like to see the syntax directive included to make editing Dockerfiles by developers more predicable.

For the harder one, I would like to see the building of a dockerfile split out as a generator. You see it today with new projects: bin/rails importmap:install, bin/rails turbo:install stimulus:install and the like.

I have multiple reasons for wanting this. Mostly because I want to make this available retroactively to Rails versions prior to 7.1, as well as to make it available to projects that upgrade to 7.1. In essence, I want to retire some code that I have that produces Dockerfiles.

I’d also like to see the generator able to be rerun on projects with existing Dockerfiles. One use would be to change an option like one of the ones mentioned above. Another is to have it scan your Gemfile and package.json to see if there are other debian packages needed to be installed. We get support requests all the time with “it works on my machine but doesn’t work with the Dockerfile you provided” when this is the root cause. The way I’d like to handle this going forward is to investigate once what is needed to install, update the generator, and tell future customers to rerun the generator when this occurs.

And, yes, I’m volunteering. And, no, the above is not an exhaustive list of the types of problems I expect Rails to be facing with this addition. And, yes, I’m volunteering to address those too.

4 Likes

Hi, I see a problem with the multi-stage builds, it is the compatibility with ARM. Do you think that multi-stage builds can be made compatible with both architectures? Otherwise, I think it is better to have a Dockerfile compatible with both architectures without multi-stage builds.

I also think that in this case simplicity is better, even if the builds take a little longer. Having a very complex Dockerfile might discourage novice developers from making modifications if they need them.

I love the idea of a Dockerfile generator based on the current state of the application. For example, an application that was created with -j esbuild but then moved to importmap.

I’m unaware of any problems with ARM architectures. In fact I routinely develop on a MacOS M1 machine. What problems are you seeing?

I like the idea of simple being the default, but I also want an easy path for those that need it.

Sorry if it’s not a problem, I haven’t tested it yet and it’s ignorance on my part. I think it might be because of this line, where x86 / x64 is assumed. Is that so? What happens with ARM in that case?

COPY --from=build /usr/lib/x86_64-linux-gnu/ /usr/lib/x86_64-linux-gnu/

Ah, good catch! Multi-stage builds work fine, but my PR has a bug.

It looks like the easiest fix will be to specify *-linux-gnu. I’ll run some tests in the morning and submit a fix.

Thanks!

I like the idea of a generator, so we make it easier to evolve the file with new dependencies. Please do explore.

Also OK to take a look at how bad the cache directives would muck up the simplicity of the file. Do pursue that in a separate PR.

I haven’t got wildcards working as a destination for a COPY, so the best I have so far is:

RUN --mount=type=bind,from=build,source=/usr/lib,target=/build \
  cp -rp /build/*-linux-gnu/* /usr/lib/*-linux-gnu

You can see what this will look like here: Optimizing your build · Fly Docs

Given that the current Dockerfile sets BUNDLE_WITHOUT, I can remove one line from that.

The full set of options to cherry pick from can be found here: Cookbooks · Fly Docs

Nearly everything there is about Dockerfiles and Rails and very little is specific to fly.io. I’d love to move whatever makes sense to a Rails site and strip the fly site to a list of differences and a pointer.

This weekend or so I’ll explore a generator in a separate repository that way I can proceed at my own pace and won’t be held up waiting for pull requests. Everything will be made available to Rails under the MIT license.

Let’s get the temp fix for COPY in right away. Since main is currently busted :+1:

Eeks on those cache directives. Shocking that it uglifies everything so much. Will review in more detail though.

Sounds good on generator :+1:

I’m waiting on this pull request being merged: Adjust whitespace and comments on Dockerfile stages by rubys · Pull Request #46966 · rails/rails · GitHub

I literally could open up a half dozen or more pull requests right now - and the merge conflicts would be spectacular. Funneling my contributions through pull requests with the delays that imposes doesn’t match well to how I develop. I’d be much happier and much more productive proceeding at my own pace and having you cherry pick some or all of the results; what’s left will find its way back to fly.io.

Merged that PR. I think we’re going at a fair pace right now! That’s, what, 5 PRs in 2 days or something :smile:. Keep 'em coming!

I submitted a pull request, and I’m not sure how I managed to create a merge conflict, but here we are. I’m heading out the door for several hours I’ll take a look at it when I get back.

I’m glad this is working for you, but I’m spending more time on attending to git and polling for merges than I am making contributions, and it is messing with my sanity.

Resolved the conflict and committed the merge.

What alternative process would you like to see?

Thanks! Next pull request is in the queue: install node modules prior to assets:precompile by rubys · Pull Request #46977 · rails/rails · GitHub . Looks like I did it right this time.

In a nutshell, push authority to railties main and edgeguides is what I seek/aspire to. To be clear, what I am not seeking is release authority, the ability to set policy, or even final say over what goes into rails. If you see something minor that needs to be corrected (recently you noted that a comment should have started with a caps), fix it. I’ll pull and rebase before my next push. And if you see something that is ugly or not acceptable, I’ll revert it and we can discuss that specific change in a pull request before it gets merged. But unless an objection is raised, I can proceed at my own pace.

I realize that I’m old and cantankerous. The first project that I got that level of authority over was over 23 years ago, and that was PHP. I’ve create and participated in dozens of projects since then, and the above is how I treat sustained contributors, and how I expect to be treated.

If that’s not meant to be, that’s fine. It just means that I will end up setting up a separate gem where I can operate like this - and will invite other to join me. And I and will encourage and welcome you to cherry pick and harvest what you like of the results into Rails.

Sam, I’ve reached out in private to resolve the workflow question. I’m sure we can figure something out, but we also have an established process for giving out commit rights that we have to respect. Appreciate all the great work you’re putting into this!

Cool. I’ll create a new repository with a railtie and dockerfile generator and invite you as a collaborator. You are welcome to contribute, harvest, or ignore. Perhaps someday that repository will be hosted on the rails organization, or perhaps someday it will have served its purpose and any useful work will have been merged into rails itself and/or be placed in a superfly repository.

Meanwhile an interesting puzzle. Take a look at Comparing rails:main...rubys:dockerfile-vendor-gems · rails/rails · GitHub

Copying system libraries makes me nervous. Because I don’t fully understand how they might be interconnected. A better approach would be to vendor the gems and have the only directory copied out of the build stage be the built application. It certainly feels cleaner.

And it works, but only if both development and test gems are excluded. It should work if only development libraries are excluded, but doesn’t. I’d like it to be able to work either way. Care to take a stab at fixing this?

Greetings!

Sam and I work together at Fly on Ruby/Rails, so I’ve been following most of the PRs and discussions.

@DHH one thing that came up in a discussion with Sam is breaking out the docker file generation into its own Rails command, like this:

rails docker:install            # Adds a Dockerfile to the project
rails docker:dockerfile         # Generates a Dockerfile to stdout
rails docker:ruby_slim_packages # Lists the packages required for the rails Dockerfile

The thinking here is that when folks upgrade to Rails 7.1, they should be able to run those commands to generate a Dockerfile for their app. As main currently stands, it’s only possible to generate a docker file during rails new.

On a slightly different topic, I’ve put a proposal together for gems to describe the packages they depend on when deployed to the ruby:#{VERSION}-slim Dockerfile. Proposal at Proposal: Declare Docker images dependencies via the `ruby_slim_docker_packages` key in `gemspec.metadata` - rails - Fly.io. This would make it easier for first-party and third-party gems to be deployable in the Rails Dockerfile without modifying the rails Dockerfile generator. This proposal wouldn’t change the work that Sam did—we still need those initial packages to bootstrap the Rails/Docker ecosystem. At the very least, I do think this would simplify some of the first-party Rails gem packages and I could see opening PRs for popular third-party gems, like Nokogiri.

Happy to open PRs for either of these if the concepts are something that would get merged into main.

It’s up: GitHub - rubys/dockerfile-rails: Provide Rails generators to produce Dockerfiles and related files.

I encourage people to try the demos: dockerfile-rails/DEMO.md at main · rubys/dockerfile-rails · GitHub

1 Like

FYI: I discovered that volta doesn’t provide arm binaries for Linux, which I consider a showstopper. I’ve switched to fnm: volta doesn't support linux arm, switch to fnm · rubys/dockerfile-rails@7a2b85c · GitHub

OK, at this point GitHub - rubys/dockerfile-rails: Provide Rails generators to produce Dockerfiles and related files. contains enough functionality for wider use by fly.io customers. My preference is that the repository be moved to the rails organization and have a much lower bar of committership than the rails repository in that organization. But you are welcome to harvest only the pieces you want, but be aware that this is likely to evolve faster than the pace of traditional Rails releases. In fact the very first person I asked to try it found a bug that applies to to the dockerfile template currently found in the Rails repository: Change from Gem.ruby_version to RUBY_VERSION · rubys/dockerfile-rails@57e066d · GitHub

1 Like

Agreed—it makes a lot of sense to have this command be its own gem to handle what I suspect will be a lot of proposals and changes to how Dockerfiles are generated for Rails as more people start using it.

I also like that it’s easier for people to add this to older versions of Rails who might not be able to upgrade to 7.1 right away. This will make testing and validation more thorough for the 7.1 release since we can get people using it now. Maybe before 7.1 releases we cut a 1.x dockerfile-rails gem?