Running Ruby in Podman (When rbenv install Fails on Apple Silicon)
![]()
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-buildAnd after adding eval "$(rbenv init -)" to my ~/.zshrc file, I tried to install the old version of Ruby:
rbenv install 2.4.10However, 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.10This 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.10However, 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 1I’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:
- set the working directory
- copy files from the source directory into the container
- create the log directory defined in
unicorn.rb - install bundler
- install gems from Gemfile.lock
- expose the port defined in
unicorn.rb - define the server start command using a
CMDinstruction
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:
- Use
0.0.0.0to connect to the container’s network interface - Define our listening port
- Set the working directory to the same value as in our
Containerfile
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::ApplicationThis is also very simple:
- After importing sinatra, define a GET route for the
/location (so this will be accessed fromlocalhost:8080/on the host machine)
$ 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!
Comments