Building Old Ruby Versions With Docker

July 1, 2022   

There are situations where we need to run old Ruby versions on modern systems. Unfortunately, as time passes it becomes increasingly difficult to build old software. This might be due to tools no longer being supported, incompatible APIs, or simply dependencies not being available.

For example, if you try to build Ruby 2.1 on a recent OS (as of 2022) you’ll very likely be disappointed. The build will probably fail because of a conflicting OpenSSL version with the one installed in your system. Ruby 2.1 (and all versions before 2.3) require OpenSSL 1.0, a deprecated version of the library.

I’m mentioning Ruby 2.1 because that was a version I had to set up for an old Rails 3.2 project but these tips should be useful for any version and library conflict you might face.

Before you continue reading make sure you’re at least familiar with how to build Docker images and with how to run containers from them. If you want to try the examples you’ll need Docker, Podman, or some kind of container runtime installed on your system.

Containers to the rescue

Docker containers are a good option to keep dependencies isolated. You can mess with them, customize your setup as much as you need and keep everything isolated from your host environment. Also, the build is very much reproducible in the future. There’s also a potential benefit in being able to deploy your images to any of the container-based cloud runtimes out there.

It’s important to know what your base image will look like. Whenever possible I start from an Alpine Linux base image because of its small size. Also one has to keep in mind if the native libraries for that base image are compatible with what we’re trying to achieve.

For the case I mentioned about of Ruby 2.1 having issues with OpenSSL 1.0 I figured while browsing through Alpine Packages that version 3.8 was the last to support that OpenSSL version. The rest was setting up the right build dependencies.

Following is a Dockerfile you can use to build Ruby 2.1.10. You can grab a pre-built image from DockerHub at manuelbarrosreyes/ruby-build-2.1. You can also build it yourself with the docker build command (assuming you’re using Docker).

FROM alpine:3.8

RUN apk --no-cache add \ 
  bash \
  build-base  \
  git \
  libffi-dev \
  openssl-dev \
  readline-dev \
  yaml-dev \
  zlib-dev

RUN git clone https://github.com/rbenv/ruby-build.git && \
  PREFIX=/usr/local ./ruby-build/install.sh && \
  rm -rf ruby-build

# Ruby Build Step
RUN ruby-build -v 2.1.10 /usr/local

If you copy and paste those contents into a Dockerfile you can run from the directory where the file is stored:

docker build -t ruby-2.1 .

and an image will be built executing all the required commands. The ruby-2.1 is an arbitrary tag I gave the image, choose whatever you like.

What we set up so far is a custom build environment. The next step would be to create a bare runtime environment that doesn’t include all the cruft from the build process.

A Smaller Runtime Image

With the image built in the previous step you can create a new one but only with the build artifacts.

For this, we use a Docker feature called multi-stage builds. This feature allows you to copy files from arbitrary images while building a new one.

FROM alpine:3.8

COPY --from=manuelbarrosreyes/ruby-build-2.1:latest /usr/local/ /usr/local/

RUN apk --no-cache add \ 
  libffi \
  openssl \
  shared-mime-info \
  yaml \
  zlib

Notice the COPY --from=... command that copies build artifacts from the referenced image.

Notice also that we’re installing non-development versions of the dependencies using apk, the Alpine Linux package manager. Those dependencies are almost the same as in the build process but without the -dev package suffix.

This image can be used as your runtime for a project that has the installed dependencies.

Conclusion

I hope these guidelines help out fellow devs stumbling with this problem regardless of the Ruby version you’re using.

I know the examples given are very specific but my goal was to focus on the ideas and concepts, not the specifics.