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.