Reviving the Old using the New

Rails 7.2 is upon us, and with it, a slew of new quality-of-life features like better production defaults and restyled documentation. But the feature we're perhaps most excited about is development containers.

Neomind's bread and butter is maintaining and upgrading legacy Rails applications. We've revived applications as far back as Rails 2 (from 2008!) and as you might guess, it's not always easy to get older versions of Ruby to build on modern hardware. Most of us are running Apple silicon based laptops, and the oldest Ruby version we can reliably get to run on an M1 mac is 2.7. So containerization is a well-worn tool in our toolkit.

But development containers takes this concept one step further. Instead of just a Dockerfile and a docker-compose.yml, development containers contain additional definitions that allow you "to use a container as a full-featured development environment.".

Starting a new Rails 7.2 app with a dev container is a simple as passing the --devcontainer option to rails new, like this:


rails new myapp --devcontainer

And if you already have a Rails 7.2 app, running rails devcontainers will add the necessary config files to let you run your app in a dev container.

But What About Legacy Apps?

This is all wonderful stuff if you're running the latest version of Rails. But for the rest of us who want to take advantage of dev containers, its a bit of a catch-22. We want Rails to generate dev containers, so we can more easily upgrade them. But we need them to be upgraded before we can easily generate dev container configurations.

Attempt #1: Using rails-new

You've no doubt run rails new before, but have you heard of rails-new? It's an executable you can run that lets you "generate a new Rails application without having to install Ruby on your machine". In theory, we should be able to use this to create a Docker container with any version of Ruby and Rails, even older versions that would not necessarily build natively on modern hardware.

The rails-new documentation suggests that it can be passed any of the options that rails new accepts. The options we're most interested in relate to specifying the Ruby and Rails versions. After installing rails-new and adding it to my PATH, I tried to create a new Dockerized Rails 4.2 app running Ruby 2.2:


rails-new legacy_app --rails-version 4.2.11.3 --ruby-version 2.2.0

This chugged along happily and created a brand new dockerized Rails app. But a closer look at the logs revealed that rails-new ignored the version options I passed and just used the latest / default versions of Ruby and Rails. Not helpful!

Attempt #2: Creating a Dev Container using VS Code

Dig a little bit into dev containers and you'll see that they are very much a Microsoft product, albeit open sourced. That means that the happy-path for using dev containers is paved with Microsoft software and services, specifically VS Code and Github workspaces. I don't love being tied in to a specific vendor or service, but for the sake of exploration, I decided to try the native Dev Containers extension for VS Code.

Once installed, I ran the "Dev Containers: Add Dev Container Configuration Files..." command to, well, add the configuration files. The extension will offer you a few choices about what exactly you want to configure, specifically whether you want just plain old Ruby on a single process or multiple services like an app, a database and a queue.

Unfortunately, the oldest option you're presented with is Ruby 3.0. I found this surprising, so I did a bit of digging. Microsoft does indeed offer a collection of Ruby dev container configurations going back as far as pre-1.0. But... no 2.2. This was strike 2.

Attempt #3: Start With Plain Old Docker, Add Dev Container Configs Later

Well, at least the Ruby community offers some read-to-roll Ruby images, in a few OS flavors, even as far back as 2.2. I figured I could use one of these instead of the images from Microsoft that were purpose-built for dev containers.

Because these images are so old (2.2 runs Debian "Jessie" by default) their packages have been archived, and typical library commands like apt-get update will fail to run. The solution is to update the OS's sources.list to point to the archived packages. You'll also need to tell the OS to not worry about package signature keys being expired. I added the lines below to my Dockerfile (after FROM ruby:2.3.4):


RUN sed -i 's/deb.debian.org/archive.debian.org/g' /etc/apt/sources.list && \
  sed -i '/security.debian.org/d' /etc/apt/sources.list && \
  sed -i '/jessie-updates/d' /etc/apt/sources.list && \
  echo 'Acquire::Check-Valid-Until "false";' > /etc/apt/apt.conf.d/99ignore-releases && \
  echo 'Acquire::AllowInsecureRepositories "true";' > /etc/apt/apt.conf.d/99allow-insecure-repositories && \
  echo 'Acquire::AllowDowngradeToInsecureRepositories "true";' >> /etc/apt/apt.conf.d/99allow-insecure-repositories && \
  apt-get update

And this did it! I can now use VS Code's dev container extention to launch and connect to this legacy app. I now have:

  • a Docker file that starts with an official Ruby image from an old Ruby version, plus some extra configurations to update the OS's packages
  • all the other configuration files necessary to run the dev container in VS code (docker-compose.yml, devcontainer.json)

Summary

What did I learn? Getting legacy Rails apps running again on modern hardware isn't trivial, but it is indeed possible. If you're OK with playing in the Microsoft ecosystem (VS Code, etc), there is a very nice official extension that makes starting dev containers easy. Most of the hard work is in finding a base container with an old Ruby version and then ensuring it can pull packages from archived locations.