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:

  1. Because I can
  2. I work with Ruby on Rails in my job every day, so I am familiar with it
  3. A learning exercise about what it is like to work with balena.io
  4. 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.

Trouble managing your Github Pull Requests?

GitArborist was created to simplify Pull Request management on Github
Mark PR dependencies, automatic merging, scheduled merges, and more →

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.