Today there are many options available to deploy apps accessible to the world. There is aws, azure, gcp as the biggest ones. Or platform as a service companies like heroku, vercel, DigitalOcean, netlify, etc.
In my professional career I've often used these services to make deployments simple and mostly not to maintain everything that goes with it. Part of that is because I don't have a lot of expertise with the deep workings of these system. I understand at a higher level what it does, but usually I'm glad I don't have to bother with it.
Recently I've been working on a personal rails app and thought about hosting it on my old raspberry pi 2, that's been collecting dust for a while. So why not try and see what there is to learn on manually deploying a docker container on this raspberry pi device myself.
Setting up your pi
To start, lets setup our raspberry pi and ensure we have all the right packages installed.
To start with a clean slate on an sd card and a fresh install of pi os, you can use this software on any os you're currently on: https://www.raspberrypi.com/software/
Note: when copying the os to the sd drive make sure to set a username and password for ssh access. This allows for easier remote access without any other monitors or peripherals connected
Ensure your os is setup and your pi connected to where you can access it through ssh
Connect to your pi through ssh and lets install some packages
In my case on macOS that would look like thisssh user@192.168.0.192
After which it will ask for your password
If you can't find the ip address of your pi you can usually find it in your routers interface by its name
Lets make sure we have all the latest packages installed and updated. Run the command:
sudo apt-get update && sudo apt-get upgrade
Installing docker is luckily a pretty simple script to download and run:
curl -fsSL test.docker.com -o get-docker.sh && sh get-docker.sh
By default, only users who have administrative privileges (root users) can run containers. However, you can also add your non-root user to the Docker group which will allow it to execute docker commands, which is what we want
To add the permissions to the current user run:
sudo usermod -aG docker ${USER}
Check it running:
groups ${USER}
Reboot the Raspberry Pi to let the changes take effect:
sudo shutdown -r now
Install docker compose using the following
sudo apt install docker-compose
Enable the Docker system service to start your containers on boot
This will setup your Raspberry Pi to automatically run the Docker system service whenever it boots up.
sudo systemctl enable docker
You can test docker by running
docker run hello-world
When successful you should see a message like "Hello from Docker!"
Deploying the rails app
To avoid installing all dependencies manually and setting up a database and other services directly on the raspberry pi, we can instead use docker. This will make for a lot more consistent and more portable app.
When you create a new rails app with rails new your_app --database=postgresql
it automatically generates with a DockerFile, making life a lot easier. At the time of writing, this is what that looks like. And according to the rails docs is production ready too.
The Dockerfile
# syntax = docker/dockerfile:1
# Make sure RUBY_VERSION matches the Ruby version in .ruby-version and Gemfile
ARG RUBY_VERSION=3.2.2
FROM registry.docker.com/library/ruby:$RUBY_VERSION-slim as base
# Rails app lives here
WORKDIR /rails
# Set production environment
ENV RAILS_ENV="production" \
BUNDLE_DEPLOYMENT="1" \
BUNDLE_PATH="/usr/local/bundle" \
BUNDLE_WITHOUT="development"
# Throw-away build stage to reduce size of final image
FROM base as build
# Install packages needed to build gems
RUN apt-get update -qq && \
apt-get install --no-install-recommends -y build-essential git libpq-dev libvips pkg-config
# Install application gems
COPY Gemfile Gemfile.lock ./
RUN bundle install && \
rm -rf ~/.bundle/ "${BUNDLE_PATH}"/ruby/*/cache "${BUNDLE_PATH}"/ruby/*/bundler/gems/*/.git && \
bundle exec bootsnap precompile --gemfile
# Copy application code
COPY . .
# Precompile bootsnap code for faster boot times
RUN bundle exec bootsnap precompile app/ lib/
# Precompiling assets for production without requiring secret RAILS_MASTER_KEY
RUN SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile
# Final stage for app image
FROM base
# Install packages needed for deployment
RUN apt-get update -qq && \
apt-get install --no-install-recommends -y curl libvips postgresql-client && \
rm -rf /var/lib/apt/lists /var/cache/apt/archives
# Copy built artifacts: gems, application
COPY --from=build /usr/local/bundle /usr/local/bundle
COPY --from=build /rails /rails
# Run and own only the runtime files as a non-root user for security
RUN useradd rails --create-home --shell /bin/bash && \
chown -R rails:rails db log storage tmp
USER rails:rails
# Entrypoint prepares the database.
ENTRYPOINT ["/rails/bin/docker-entrypoint"]
# Start the server by default, this can be overwritten at runtime
EXPOSE 3000
CMD ["./bin/rails", "server"]
However, rails usually does not work on its own without a database (and redis in case you want to use some async workers like sidekiq). In our case that database is postgres.
To start rails alongside the database the simplest way is to use docker composer. Docker compose lets you combine different services and connect them together.
Create a new file in the root of your project called docker-compose.yml
with the following content:
version: '3.7'
services:
db:
image: 'postgres:latest'
volumes:
- postgres:/var/lib/postgresql/data
environment:
- POSTGRES_DB=api_production
- POSTGRES_USER=api
- POSTGRES_PASSWORD=[your-password]
web:
environment:
- SECRET_KEY_BASE=[some-key]
- API_DATABASE_PASSWORD=[your-password]
build: ./
ports:
- '3000:3000'
volumes:
- .:/docker/app
depends_on:
- db
volumes:
postgres: # named volume
One last thing to update is our production database connection in rails in database.yml
and make sure the production key looks like this (or development if you want to use docker for development)
production:
<<: *default
database: api_production
username: api
password: <%= ENV["API_DATABASE_PASSWORD"] %>
host: db # this links the docker database service
One last thing we need to do in order to build on the raspberry pi 2 is to add this line in the Gemfile.loc
. This section allows it to be build on that specific os. If you're using a different os and raspberry pi, you might need a different one. But it will give you the appropriate error message
PLATFORMS
arm-linux-eabihf
After this is all setup we can run the following:
docker compose up --build
The docker command should automatically detect the docker-compose.yml
file and start building the image. The --build
argument ensure that it rebuilds in case there's any changes to the ruby code.
The first time this will take a while and will download several containers. After the first time, those should be cached and will be significantly faster.
If you want to run the docker image detached you can add a -d
to the command to run it detached and keep the image running when disconnecting from the pi.
You should be able to access your app on the same ip as the raspberry pi with port 3000 (unless otherwise specified):
Cleanup
To clean up your docker build simply run
docker compose down
And that should stop the container and its services.
Closing thoughts
None of the steps included a git repository, but would be greatly helpful of course.
Most likely you would develop the app locally on your local device and test it out. Whenever you make any changes, you can push those up to your repo.
On your raspberry pi you can then pull those changes down and rerun docker compose up --build
to update your image with the latest code changes and database migrations.