So you’re working on a Rails upgrade in a pretty big app that has lots of active development. The app’s pretty far behind Rails versions—let’s say it’s on Rails 4.2. It’s tempting to upgrade straight to the latest Rails version, but you decide to take an incremental approach and upgrade to the next point release, Rails 5.0.
You have a feeling this upgrade is gonna take a while, and instead of trying to maintain a long-lived branch, you choose to go with a dual booting strategy. You do this with the Bootboot Bundler plugin and have dual booting setup in no time. You open a PR with the changes and wait for a green build, but before any tests are run, you see a weird error:
Unable to find a spec satisfying bootboot (>= 0) in the set. Perhaps the lockfile is corrupted?
You’re most likely seeing this error because your CI environment is installing gems with Bundler’s deployment flag. One of the things the deployment flag does is freeze the
Gemfile against gem versions specified in
Gemfile.lock so gems can’t be changed (you generally don’t want unexpected gem changes to happen as part of the deploy process). Since Bootboot is a Bundler plugin, it gets installed every time the
Gemfile is evaluated. The
Gemfile isn’t frozen in development, so Bootboot gets installed with no problem. It is frozen on CI, however, so you get an error. 😕
Here’s the good news—this is a known issue! And it’s been fixed!! 🥳 Bundler has been updated to allow plugin installs when the
Gemfile is frozen. Now we just need to upgrade Bundler, right?
Here’s the bad news. That change was released in Bundler 2.0.2, but you can’t use Bundler > 2.0 on Rails 4.x. 😭
The Bundler restriction is relaxed on Rails 5.0, allowing Rails to be used with Bundler 2+. If you’re on Rails 5+, try upgrading Bundler to get past this issue. But if you can’t upgrade Bundler, read on!
To workaround this issue, the Bootboot README recommends disabling Bootboot when the
Gemfile is frozen. You’d do this in the
plugin 'bootboot' unless Bundler.settings[:frozen]
This gets rid of the error, but now you won’t be able to use Bootboot in environments where gems are frozen. This kinda defeats the point of using Bootboot though, right? You’ll be able to dual boot Rails 4.2 and Rails 5.0 in development, but you won’t have a way to do it in CI, staging, production, etc.
If your deployment environments are Dockerized, there are a few tricks you can use to get this thing working. To demonstrate, we’ll use the following
Dockerfile as a reference.
Rails will soon ship with more Docker support. New Rails apps will be generated with a
Dockerfile that can be used as a starting point for production environments. There’s also a new Docked Rails CLI for local development in Docker.
# syntax=docker/dockerfile:1 FROM ruby:2.7 WORKDIR /myapp # Install Bundler RUN gem install bundler:1.17.3 # Install application gems COPY Gemfile Gemfile.lock ./ RUN bundle install --deployment # Copy application code COPY . . # Add a script to be executed every time the container starts. COPY entrypoint.sh /usr/bin/ RUN chmod +x /usr/bin/entrypoint.sh ENTRYPOINT ["entrypoint.sh"] EXPOSE 3000 # Configure the main process to run when running the image CMD ["bin/rails", "server", "-b", "0.0.0.0"]
In the first part of the
Dockerfile, we’re copying the
Gemfile.lock to the container’s working directory and installing the app’s gems with the deployment flag.
# Install application gems COPY Gemfile Gemfile.lock ./ RUN bundle install --deployment
We still have the workaround that prevents Bootboot from being installed when the
Gemfile is frozen. So
- Bootboot won’t be installed in the container
- The container currently only has the Rails 4.2 gems installed
Now we’ll change the
Dockerfile, so the container also has Rails 5.0 gems installed. If Bootboot were available, we’d only need to add
COPY Gemfile_next.lock ./ RUN DEPENDENCIES_NEXT=1 bundle install --deployment
DEPENDENCIES_NEXT is set, Bootboot will tell Bundler to use the Rails 5.0 gems by
- using the Rails 5.0 gems specified in the
if ENV[DEPENDENCIES_NEXT]section of the
- resolving Rails 5.0 gem versions against
Without Bootboot, the first step will still work as expected because it’s not specific to Bootboot. Bundler will evaluate whatever Ruby code is in the
Gemfile. The second step will fail though. Without Bootboot, Bundler assumes the lockfile is named
Gemfile.lock as it matches
Gemfile. Bundler would then resolve the Rails 5.0 gem versions against
Gemfile.lock that has Rails 4.2 gem versions. It ain’t gonna work.
We need to tell Bundler to use
Gemfile_next.lock and not
Gemfile.lock when resolving Rails 5.0 gem versions. Bundler doesn’t expose a way to specify what lockfile to use, but we can tell it what
Gemfile to use. 😁
We’ll add a few lines to the
Dockerfile. After installing the Rails 4.2 gems we’ll do
COPY Gemfile_next.lock ./ COPY Gemfile ./Gemfile_next RUN DEPENDENCIES_NEXT=1 BUNDLE_GEMFILE=Gemfile_next bundle install --deployment
This will add two files to the container: a copy of
Gemfile_next.lock and a copy of the
Gemfile_next. Then, we’ll get Bundler to use
Gemfile_next.lock by telling it that the
Gemfile_next. And now our container will have both Rails 4.2 and Rails 5.0 gems installed. 🥳
Finally, we need to expose a way to run the containers with Rails 5.0 gems. Looking back at our
Dockerfile, we can see there’s an
entrypoint.sh script that gets run every time the container starts.
# Add a script to be executed every time the container starts. COPY entrypoint.sh /usr/bin/ RUN chmod +x /usr/bin/entrypoint.sh ENTRYPOINT ["entrypoint.sh"]
entrypoint.sh we’ll use
DEPENDENCIES_NEXT to set
BUNDLE_GEMFILE before executing the container’s command.
#!/bin/bash if [[ -v DEPENDENCIES_NEXT ]]; then export BUNDLE_GEMFILE=Gemfile_next else export BUNDLE_GEMFILE=Gemfile fi # Exec the container's main process (what's set as CMD in the Dockerfile). exec "$@"
This lets us abstract away the fact that Bootboot isn’t available in the containers. When
DEPENDENCIES_NEXT is set, we’ll set
Gemfile_next, so the Rails 5.0 gems get used. Otherwise, we’ll set
Gemfile and use the Rails 4.2 gems by default.
And with that, we can effectively use Bootboot when the
Gemfile is frozen on Rails 4.x (and pre-Bundler 2.0.2). 🎊
Your Docker setup will probably be different, but the gist here is:
- Install the production gems as normal
- Make a copy of the
- Install the Rails upgrade gems by setting
DEPENDENCIES_NEXTis set, set
Gemfile_next. Otherwise, set it to
When working on Rails upgrades for big, old apps, you’ll likely run into situations like this where new things don’t work with old things. And it can be a very frustrating experience. But with a little bit of patience, fortitude, and creativity you can usually get the computers to do what you want ’em to do.
h/t to this Bootboot issue for giving me faith that there had to be a way to get this to work