Go to article URL

I recently worked on a legacy Ruby backend app which hadn’t been changed for years. A Ruby development environment used to be provided by DevCamps for this site, but over the years we stopped using Ruby other than this small backend, so we also stopped using camps.

The frontend development now uses its own local dev server, so I decided to do the same for the backend by running the Unicorn server locally. I’m on a MacBook M2, so I should just be able to install rbenv using Homebrew. Easy, right?

Unfortunately not. I had a great deal of trouble getting any version of Ruby to run natively on my MacBook M2, so I eventually resorted to using Podman. You can skip to the end for my solution.

First try: native rbenv

The rbenv installation went just fine:

brew install rbenv ruby-build

And after adding eval "$(rbenv init -)" to my ~/.zshrc file, I tried to install the old version of Ruby:

rbenv install 2.4.10

However, this returned a BUILD FAILED message. Strangely, it also failed with a new version of Ruby (3.3.10). I tried debugging for a while before giving up — I figured this project isn’t worth multiple hours of getting a local Ruby installation to work (despite my obstinate attempts to do so).

Second try: mise

Several people on forums & blogs recommended using mise, a language-agnostic version manager. After installing it with Homebrew, I ran:

mise use --global ruby@2.4.10

This failed too! There was a good explanation, though: older Ruby versions don’t run on ARM processors. Of course, that makes sense. So I just needed to install a newer version from after Apple silicon was supported:

mise use --global ruby@3.3.10

However, this failed as well, just like rbenv:

mise ERROR Failed to install core:ruby@3.3.10:
   0: ~/Library/Caches/mise/ruby/ruby-build/bin/ruby-build exited with non-zero status: exit code 1

I’m sure there is a way to get Ruby running through a version manager on Apple silicon; I found several tutorials claiming you can just run these commands as normal and it’ll work. However, after spending longer than I’d like to admit, I was completely unable to do so despite trying many fixes found online.

This may be a unique problem on my machine, or I may be missing something. But the fact was, I’d waited through about a dozen failed Ruby installs (which take a long time!) and I just needed the app to work now. So I moved to a third option: run Ruby in a container.

Third try: Podman

Podman is a fully open-source containerization system which can run Dockerfiles and docker-compose.yml files. The app is a lightweight Sinatra backend with Unicorn as a server, and I just needed to add CloudFlare Turnstile to reduce bot traffic.

The app uses two main files: unicorn.rb (which holds the configuration for the Unicorn server) and config.ru (which defines and runs the Sinatra app). The command to start the app is pretty simple: bundle exec unicorn -c unicorn.rb config.ru. I also set up two files for the container: Containerfile and container-compose.yml.

Note: Podman does find files named Dockerfile and docker-compose.yml, but here I’m using Podman’s convention of Containerfile and container-compose.yml, which takes precedence if both are present in the folder.

To run this, I just needed to set up the environment in my Containerfile.

Containerfile

FROM ruby:2.4.10

WORKDIR /usr/src/app

COPY . .

RUN mkdir -p /var/log && \
 gem i bundler && \
 bundle install

EXPOSE 8080

CMD ["bundle", "exec", "unicorn", "-c", "unicorn.rb", "config.ru"]

It’s a pretty simple setup:

container-compose.yml

services:
  backend:
    build:
        context: .
        dockerfile: Containerfile
    volumes:
        - .:/usr/src/app
    ports:
        - "8080:8080"

The volumes section means that changes on the host machine will be propogated to the container (and vice versa). This means you don’t need to rebuild the container when you change the app, instead you can use a change-watching Gem like rerun.

Note: You can run this without a compose file, the compose file just stores the volume & port information so you can run it more easily.

podman run -d --name mybackend -v .:/usr/src/app -p 8080:8080 <image_id>

Minimal unicorn.rb configuration file

listen "0.0.0.0:8080"
working_directory "/usr/src/app"

This is simple:

Note: When we forward port 8080 to our Podman container, that traffic arrives on the container’s network interface, not on its internal loopback (localhost). If your app only listens on localhost, it’s essentially saying “I only accept connections from myself,” so the forwarded traffic gets rejected. By binding to 0.0.0.0, your app listens on all interfaces, which includes the one Podman uses to route external traffic into the container.

Simple config.ru

require 'sinatra'
get '/' do
  "Hello from Sinatra in Podman!\n"
end
run Sinatra::Application

This is also very simple:

$ podman build .
$ podman run -p 8080:8080 <image-id>

This returned a successful response on my host machine:

$ curl localhost:8080
Hello from Sinatra in Podman!

When the app failed to run, it kept trying to restart until I moved a .env file into place and it worked. That shows that the volume is working correctly.

I’m not a Ruby expert, but with this setup I was able to convert the JavaScript code for Turnstile to Ruby and get it working!

www.endpointdev.com/blog/feed.xml
programming | reporting