In this post, I’m going to take a look at what it takes to get the latest version of Ruby on Rails (at the time of writing that is Rails 6.0.0 on Ruby 2.6.3) running on a Rpi Zero W.
First, let’s answer the burning question of why would anyone do this:
- Because I can
- I work with Ruby on Rails in my job every day, so I am familiar with it
- A learning exercise about what it is like to work with balena.io
- An experiment about what is possible with the balena.io infrastructure beyond what they support out-of-the-box
Let me also get this out of the way: Ruby-on-Rails is not particularly suited for embedded devices, so I would probably never want to do this in a real-world scenario. balena already offers pre-baked images for Rust, Go, C++, Node, etc. All of which are probably better suited to the job.
Now, with that disclaimer in place, let’s get to it.
balena already has a set of instructions for setting up a device so I won’t rehash that here. For our purposes, the Nodejs tutorial will work perfectly.
After going through the tutorial you should have a
simple-server-node directory on your machine.
The final Dockerfile is available as a Github gist here, but I’ll include a breakdown to explain what’s going on.
balena supports multiple architectures, but for simplicity, I’m ignoring that so in your
simple-server-node directory you can delete
Dockerfile.template and create a new file called simply
I’m not going to give a full run-down of Dockerfiles here, just some commentary on what this particular one is doing. First, we will start from balena’s existing node image for the Raspberry Pi:
10 is the nodejs version and
buster is the Debian Linux version.
Available options can be found in balena’s base-images Github repo.
We have two options here, we can use the version provided by the various repositories (at the time of writing,
install_packages ruby will install Ruby 2.5.5 on Rpi devices).
Or we can install Ruby ourselves as part of the build process. Ruby does not provide pre-built binaries, so we will need to download the source code and compile it ourselves.
RUN mkdir -p /tmp/ruby \ && curl -SLO "https://cache.ruby-lang.org/pub/ruby/2.6/ruby-2.6.3.tar.gz" \ && echo "577fd3795f22b8d91c1d4e6733637b0394d4082db659fccf224c774a2b1c82fb ruby-2.6.3.tar.gz" | sha256sum -c - \ && tar -xzf "ruby-2.6.3.tar.gz" -C /tmp/ruby --strip-components=1 \ && rm -f ruby-2.6.3.tar.gz
At the time of writing 2.6.3 is the latest Ruby version (2.6.4 introduced a string corruption bug so is not reliable), so that’s what we’re using here, but the process will be pretty much identical for any version. The commands here basically do the following:
- Create a new directory at
/tmp/rubyso we have somewhere to put the files
- Download the tarball Ruby source
- Validate the download is correct by checking the shasum of the file (optional)
- Extract the source code into
- Remove the downloaded tarball as we no longer need it
Now that we have the source we need to compile it:
WORKDIR /tmp/ruby RUN ./configure \ && make \ && make install
We change to our new directory with the Ruby source, then run the
make install commands.
These are all straight from Ruby’s documentation on how to build from source.
Congratulations, your image will now have Ruby 2.6.3 ready to go, all that’s left is to do a little cleanup by removing the Ruby source files thusly:
WORKDIR /tmp RUN rm -r /tmp/ruby
Now we can get things set for Rails.
The only other thing we need for Rails is
bundler, which we can install using
gem now that Ruby is on the machine:
RUN gem install bundler
Now, we need to create a Rails application.
There’s no shortage of tutorials about how to install Rails and create a basic app so I won’t repeat all that here.
Suffice to say all we need at this stage is to run this command inside our
rails new testapp
This will create a new stock-standard Rails 6 app (assuming you have Rails v6 installed) inside a new
Docker changes for Rails
Now that we have a basic Rails app, we can add the last few items to our
First, we will copy the Rails app into the Docker image:
WORKDIR /app COPY /testapp ./
With that done, we need to pull in all of Rails’ dependencies, using Bundler (which we installed earlier), and Yarn (which is already present on balena’s Nodejs image):
RUN bundle install RUN yarn install
Now we have a few configuration options to set for Rails. We can limit it to only use 1 thread (since the Pi Zero is single-core), and have it listen on the standard port 80 for http traffic:
ENV RAILS_MAX_THREADS 1 ENV PORT 80
Lastly, there is the command to actually launch the Rails server.
We need to also specify we want it to allow any incoming IP addresses (because we are running in
development mode, which by default only allows connections from the local machine for security.)
CMD bundle exec rails s -b 0.0.0.0
Adding all these changes to git, you should be able to push up the code and have it deployed to your Raspberry Pi (eventually).
Once it loads (this will take some time due to how underpowered the Pi Zero is for this job), you should be able to load
http://<your pi's IP> in your browser and see the “Yay! You’re on Rails!” welcome screen.
balena also allows accessing the device via URL from outside your local network, but our current configuration does not allow that.
Rails will show you a screen with instructions to fix this, but in essence, you simply add a line like this to
testapp/config/environments/development.rb before the last
end in the file:
config.hosts << "<your pi's public url>"
Let’s be honest here, this was more of a learning excerise than a long-term app hosting plan. That said, while the Pi Zero is pretty lightweight for this task, something like the new Raspberry Pi 4, with a quad-core 1.5Ghz CPU + up to 4Gb RAM could actually make this a viable contender for hobby projects, given a similar spec’d Virtual-Private-Server would cost around $15-20 per month on something like DigitalOcean (affiliate link) (if you ignore HDD storage, though you could certainly hook up an external drive to the Pi).
Overall I’m very impressed with how streamlined balena makes the process for deploying code onto embedded devices, and how flexible it is for customisation due to its use of Docker.