October 7, 2024

A 10 year old app running on a modern Mac? Yes, you can!

We recently had a really old app roll into our shop: a Rails 4.1 app, running Ruby 2.3. The git history was a bit convoluted, but the app is definitely no younger than 10 years old. How do we get it running on a Mac with Apple silicon? Dev Containers to the rescue!

After cloning the repo locally, we open it up in VS Code. We add a new hidden folder, `.devcontainer` to the root of the project, and then add four files:

The Dockerfile

Our `Dockerfile` will start with the official `ruby:2.3.4 image`, which uses Debian Jessie.

FROM ruby:2.3.4

Jessie is end-of-life, so apt-get won't work and you won't be able to use it to install packages. Fortunately, there is a work-around. We can manually update the OS's package source list to pull from the archives. We can also disable key checking, as the signature keys will most definitely be expired:

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

Next, our app also relied on the TinyTDS gem, which needs the FreeTDS package. These lines will download and build it for us:

RUN DEBIAN_FRONTEND=noninteractive apt-get install -y --allow-unauthenticated wget build-essential libc6-dev && \
    wget http://www.freetds.org/files/stable/freetds-1.4.10.tar.gz && \
    tar -xzf freetds-1.4.10.tar.gz && \
    cd freetds-1.4.10 && \
    ./configure --prefix=/usr/local --with-tdsver=7.4 && \
    make && \
    make install

# These lines ensure that environment variables persist for subsequent RUN commands (neccesary for the instalation of the tiny_tds gem - see above).
ENV PATH="/usr/local/bin:$PATH" \
    C_INCLUDE_PATH="/usr/local/include:$C_INCLUDE_PATH" \
    LIBRARY_PATH="/usr/local/lib:$LIBRARY_PATH" \
    LD_LIBRARY_PATH="/usr/local/lib:$LD_LIBRARY_PATH"

The Docker Compose File

Our `docker-compose.yml` file is pretty standard. We're using the latest PostgreSQL 9.6 image - later versions have security features that don't play well with this old app.

Also worth noting: this will create a new volume, `postgres-data`, to persist the database data. It will also copy a database initialization SQL command (`.devcontainer/create-db-user.sql`)

services:
  app:
    build:
      context: ..
      dockerfile: .devcontainer/Dockerfile

    volumes:
      - ../..:/workspaces:cached

    # Overrides default command so things don't shut down after the process ends.
    command: sleep infinity

    # Runs app on the same network as the database container, allows "forwardPorts" in devcontainer.json function.
    network_mode: service:db
  db:
    image: postgres:9.6.24
    restart: unless-stopped
    volumes:
      - postgres-data:/var/lib/postgresql/data
      - ./create-db-user.sql:/docker-entrypoint-initdb.d/create-db-user.sql
    environment:
      POSTGRES_DB: postgres
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres

volumes:
  postgres-data:

Here's what we put in the `.devcontainer/create-db-user.sql` file. Note that dev containers, by default, are run as a user called `vscode`.

CREATE USER vscode CREATEDB;
CREATE DATABASE vscode WITH OWNER vscode;

The Dev Container JSON File

Finally, the `devcontainer.json` file includes some basic dev container configurations. Note that we're using the `postCreateCommand` section to install the gems (`bundle install`) and to initiate the database.

{
	"name": "Ruby on Rails 4.1 & Postgres 9.6",
	"dockerComposeFile": "docker-compose.yml",
	"service": "app",
	"workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",

	// Features to add to the dev container. More info: https://containers.dev/features.
	// "features": {},

	// Use 'forwardPorts' to make a list of ports inside the container available locally.
	// This can be used to network with other containers or the host.
	// "forwardPorts": [3000, 5432],

	// Use 'postCreateCommand' to run commands after the container is created.
	"postCreateCommand": "bundle install && bundle exec rake db:migrate db:seed"

	// Configure tool-specific properties.
	// "customizations": {},

	// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
	// "remoteUser": "root"
}

App Tweaks

Naturally, there will be some small app tweaks necessary to get the app running properly, but as a general goal, we tried to make as little changes as we could to the app to get it to run. Here's what we found:

Internalizing Unsupported Gems

We found two very niche and abandoned gems that the app relied on. In the interest of maintainability, we internalized the gems by copying them into the `/vendor/gems` folder.

Where our `Gemfile` used to have this:

gem 'weird_gem', :git => 'https://github.com/author/weird_gem.git', :branch => 'weird-branch'

we now just refer to the local copy of the gem:

gem 'weird_gem', path: 'vendor/gems/weird_gem'

Add Long-Lost Dependency Gems

This app uses the `paperclip` gem, which has long since been abandoned, but at least still exists on Github. However, one of its dependencies, `mimemagic`, specifically version `0.3.0` is no longer available as a release. We had to dig through that gem's commits to find the commit at that specific version, and refer to that in our Gemfile, which now looks like this:

gem 'mimemagic', git: 'https://github.com/mimemagicrb/mimemagic', ref: 'a4b038c6c1b9d76dac33d5711d28aaa9b4c42c66'

What Version of Ruby Were We On Again?

The app's `Gemfile` and `.ruby-version` both indicated that the app was running Ruby 2.3.4, but the app also used an older version of the AuthLogic gem that relied on the `unpack1` method, which wasn't available until Ruby 2.4. Instead of upgrading Ruby just to get that method (which could have introduced other side effects), we added a temporary monkey-patch to add that method as a initializer:

# config/initializers/unpack_patch.rb
unless String.instance_methods.include?(:unpack1)
  class String
    def unpack1(format)
      unpack(format).first
    end
  end
end

More AuthLogic Shenanigans

AuthLogic wasn't done with us yet. It was trying to call a `with_scope` method, which was removed after Rails 3.1. The best choice in this case was to simply upgrade AuthLogic to a later minor version (3.3 to 3.6) that didn't call that missing method anymore.

Success!

And that did it! With a dev containers, and a few minor app code tweaks, we were able to get this very old app running on modern hardware.

← Back to Blog