Rails Dockerfile futures

I don’t know how to interpret the lack of response since I’ve posted the generator. I plan to put this in production next week. If you have any feedback, let me know. Meanwhile, I’ll continue to watch the Rails repository for changes.

Got dragged into some other tasks. Will review soon. But great to just continue rapid iteration on the repo now, then when I have another opening I can review in bulk :+1:

The generator now has a growing test suite. Perusing the expected results can quickly get you an overview of what the generator is capable of:

If you want to know what combination of options were used to produce any given results, look at the corresponding test case here:

The latest dockerfile-rails gem will write out options used to config/dockerfile.yml, and will use those options as the default for the next run.

If Rails new uses this gem, it can pick default options.

I will be posting a webpage suggesting different sets of options that may work well with fly.io. Other cloud hosting services can do likewise.

And, of course, developers can ignore all of our advice(s) and select their own preferences.

For many that will mean that upgrading to a new version of Ruby or adapting after installing a new gem that requires a library to be installed will be a matter of:

bin/rails generate dockerfile

… accepting the changes (or specifying --force) and committing the results.

2 Likes

Here’s the fly docs on options: Dockerfiles and fly.toml · Fly Docs

The plan is to go live on Sunday: Cut over to Rails Dockerfile Generator on Sunday 29 Jan 2023 - rails - Fly.io

What this means is that I believe the gem is feature complete at this point. It also means that it is about to get significant real world usage and testing.

1 Like

Just checking in. What fly.io needs (and I strongly suspect mrsk would greatly benefit from) is a dockerfile generator that can be run on existing projects, and rerun as dependencies change, rather than a providing a starter Docker image after which you are on your own.

  • I’ve fixed many bugs. I’ve stopped mentioning them here as I haven’t seen interest in making the fixes. An example: Gem.ruby_version can produce a four part version number, but ruby-slim docker images only have three part version numbers.
  • libvips is not the only debian package that Rails applications might need. There are many popular packages with similar dependencies. Packages like grover and execjs require even more.
  • I’ve seen many “API-only” applications with a JS front end that, from a deploy, perspective are very much like jsbunding rails applications, but running npm run build rather than assets:precompile. Some seem to like structuring their repository as a mono-repository with subdirectories containing gems or engines. We should support these types of architectures too.
  • being able to run the generator when the project is 'ready", and rerun it as needed, means that the Dockerfile can include only what is needed by the application and/or explicitly requested rather than including what might be needed down the road. This results in smaller images and faster load times.
  • There are plenty of build and runtime optimizations that can be opted into. And I’ve got more planned. As an example, some believe that best practices is to not run your rails application as root. I’m fine leaving the default as root, but by specifying an option a “ruby” or “rails” user can be created and the ENTRYPOINT/CMD can be run under this user.

I truly believe that this belongs some place more permanent than my personal github; if you agree lets find a home for it. If not, that’s fine and I’ll move it to the superfly organization, but continue to make it available to all.

1 Like

Will definitely review before we get on the release train with 7.1. I remain in favor of the broad concept, but I also want to be careful this doesn’t turn into a huge thing. If someone wants to get into running under a separate user, that to me is outside the scope of what Rails needs to provide as a starting point. Anyway, thanks for working on this!

1 Like

On the topic of separate user, that seems to be a top recommendation of pretty much every page I can find via google searches of “Dockerfile security best practices”. Additionally for those users that wish to run docker containers that mount volumes from their host drive, matching their host UID/GID will often make things easier. For those reasons, it seems reasonable to me to provide an option to allow running rails under a user other than root. It only ends up being a few extra lines in the Dockerfile if you select this option, and no extra lines at all if you don’t.

In any case, you continue to be welcome to make use of the gem to provide a Dockerfile (which I recommend), harvest just the pieces that you find relevant, or ignore it completely.

On a personal note, decoupling my ability to make forward progress from your availability has done wonders for my sanity.

2 Likes

A few issues

Glad to see that the Rails template now includes a user.

Also, I fully agree with Inspiration is perishable

It looks like @DHH would like to see us stick to a single Dockerfile generated at “rails new” time.. I’ve written down why I don’t think that will work for most Rails developers in motivation.

Meanwhile, some additional thoughts:

  • volta doesn’t work on ARM machines, so the current Rails dockerfile can’t be run on a Mac M1/M2 machine if you select a --javascript option on rails new.
  • setting BUNDLE_PATH="vendor/bundle" enables you to eliminate the copy of /usr/local/bundle resulting in a significantly smaller image size without making the dockerfile any more complicated
  • running out of memory is a real problem, particularly for those trying out a free tier of a cloud offering. It doesn’t make for a very good first impression.
  • while build cache and other options produce less readable Dockerfiles they solve real problems. I’m fine with them not being the default, but they should be easy to opt in to.

In any case, I am working on dockerfile-rails because I need it. Everybody is welcome to either make use of it directly or steal the pieces they need.

@Sam_Ruby If you’re looking for an end to end solution that handles running things as a non-root user with an ability to customize the UID / GID at build time (defaults to 1000:1000) there’s docker-rails-example/Dockerfile at main · nickjj/docker-rails-example · GitHub.

It works out of the box for most set ups where it’s expected you may have volumes, including:

  • Docker Desktop
  • Native Linux where the first user on your system is the user running Docker commands
  • Any deployment environment where you control the machine (such as a single server VPS deploy)

If needed you can set UID= and GID= to be whatever custom value you need and rebuild.

I wrote a guide / video about it here: Running Docker Containers as a Non-root User with a Custom UID / GID — Nick Janetakis

I’ve been running this set up for a while now, it works. Lots of folks who have gone through my Docker courses have used this on a range of different systems without issues.

The approach Rails is currently taking can be found here:

dockerfile-rails matches this approach unless generation of a docker-compose.yml is requested, then it produces results that I believe matches your guide. If you think either approach can be improved, I encourage you to create an issue or (better yet) a pull request in the relevant github repository.

I did create issues about this in rails/rails with an in depth break down with suggestions to improve the Dockerfile but it got closed pretty quickly. A few Rails core members didn’t seem enthusiastic with wanting to use -slim, multi-stage builds and setting a non-root user – they said all of this can be solved with documentation.

By the way you can reduce RUN layer steps in your set up by adding that RUN useradd rails to the previous RUN layer. There’s no negative to that since creating a user takes 1 second. The upside is by reducing the layer count your image will build faster (although in this cause it’s minor, but it’s still an improvement).

I’ve ran something similar to your set up for years in the past but you’ll encounter a number of issues, not setting --create-home may cause issues with packages that depend on a home dir existing, not setting USER as an env var will be an issue as well. Not setting --no-log-init may cause massively sized Docker images in some cases and folks who don’t have 1000:1000 as their UID/GID won’t be able to use the image without permission issues.

All of these are addressed in the link I supplied above btw.

I’d just like to say a big “thank you” for your work on dockerfile-rails! I will continue using it rather than the default in 7.1. As a busy solo developer I have been trying on and off over many years to get a working production-ready Docker configuration. I’ve read many books, articles and watched many videos. But your clear and concise project is what’s finally taken me over the top to where I have finally achieved the goal! This is a very worthwhile project whether it’s included with Rails or not. I came to your project after experimenting with the new Rails 7.1 Dockerfile and realizing that it was several steps behind what I already knew, then I found your project. I think this will be a natural progression for many users.

1 Like

I’ve encountered the same, and it takes perseverance. I’m in this for the long haul. I’m pleased to say that all of the issues you mentioned are now part of the Rails 7.1 template.

I will say that you won’t find this attitude in dockerfile-rails. I encourage pull requests and issues, particularly ones that check the actual state of the project or with flags to opt in when the recommendations don’t universally apply. Not everybody will need every possible recommendation, but making it easy to generate Dockerfiles to match different sets of configurations enables rapid experimentation and is a substantially better developer experience than making every change a research project which is what sending people to documentation ends up being.

Thanks for the kind words! I will say that I’m still learning, and the outcome of this discussion in particular will result in a number of improvements. I suggest that you periodically upgrade and regenerate your Dockerfile to take advantage of improvements as they make their way into dockerfile-rails.

Yes I’ve been doing this regularly as new versions are released!

1 Like

I wanted to share a project that I’ve been working on to provide a turn-key Dockerfile UX for Rails apps: railbarge.

railbarge is a set of Docker base images that use ONBUILD instructions to perform build steps which are tailored to the app. So the following Dockerfile will work as is for many Rails apps:

ARG RUBY_VERSION=3.2.1

FROM railbarge/builder:ruby-$RUBY_VERSION as builder

FROM railbarge/app:ruby-$RUBY_VERSION
COPY --from=builder /artifacts /

I also wanted to say thank you, @Sam_Ruby, for all of your work. I have learned a ton from your pioneering, and I deeply appreciate it. I especially appreciate the demos for dockerfile-rails. They have been an extremely valuable resource.

Regarding railbarge, the minimal demo, the Action Cable demo, and the esbuild demo all run with the above Dockerfile as is.

The Neofetch demo requires the neofetch apt package, so that needs to be added to the Dockerfile:

ARG RUBY_VERSION=3.2.1
ARG RUNTIME_PACKAGES="neofetch colorized-logs"

FROM railbarge/builder:ruby-$RUBY_VERSION as builder

FROM railbarge/app:ruby-$RUBY_VERSION
COPY --from=builder /artifacts /

The Create React App demo requires the React app to be built with npm run build, so that needs to be added to the Dockerfile:

ARG RUBY_VERSION=3.2.1
ARG PACKAGE_JSON_DIR=client

FROM railbarge/builder:ruby-$RUBY_VERSION as builder
RUN cd client && npm run build && mv build/* ../public

FROM railbarge/app:ruby-$RUBY_VERSION
COPY --from=builder /artifacts /

And the Grover demo uses Puppeteer, which requires Chromium and Node.js at runtime, so those need to be added to the Dockerfile:

ARG RUBY_VERSION=3.2.1
ARG RUNTIME_PACKAGES="chromium"

FROM railbarge/builder:ruby-$RUBY_VERSION as builder
RUN dockhand install-node --prefix=/artifacts/usr/local

FROM railbarge/app:ruby-$RUBY_VERSION
ENV PUPPETEER_EXECUTABLE_PATH="/usr/bin/chromium" \
    GROVER_NO_SANDBOX="true"
COPY --from=builder /artifacts /

Feedback is welcome! If anyone has a use case that isn’t covered by the dockerfile-rails demos, please do share! It may benefit dockerfile-rails too!

1 Like

Seriously impressive. We should try to find a way to collaborate. Things are still moving fast - just today there was a major change to the Rails Dockerfile concerning security.