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.
Starting point
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.
Rails 6 requires Nodejs to handle the javascript-end of things, so we can re-use balena’s node image to get us started. From there on out though we will be using a custom Dockerfile to achieve what we want.
Dockerfile
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 Dockerfile
.
Base Image
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:
FROM balenalib/raspberry-pi-node:10-buster-build
Here 10
is the nodejs version and buster
is the Debian Linux version.
Available options can be found in balena’s base-images Github repo.
Installing Ruby
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/ruby
so 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
/tmp/ruby
- 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 configure
, make
, and 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.
Installing Bundler
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.
Rails
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 simple-node-server
:
rails new testapp
This will create a new stock-standard Rails 6 app (assuming you have Rails v6 installed) inside a new testapp
directory.
Docker changes for Rails
Now that we have a basic Rails app, we can add the last few items to our Dockerfile
.
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
Deploy
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.
Public URL
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>"
Conclusion
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.