Rails on Docker
Rails and Docker are important components in the development processes at Zegetech. Rails is our chosen platform for most of what we build, and docker provides pain-free environment management both for development and in production. We have previously covered these two technologies separately, and this post covers the sweet spot at their intersection. We will take you through the process of configuring a rails development environment on docker, and configure a postgresql database for it.
You might want to take a look at our previous blogs for some background information on what we will be setting up.
Our goal
We want to build an app and have chosen Ruby on Rails as our framework. But because docker is awesome and a key component in developing and keeping things clean, we want to set up a development environment that allows us to work on a Rails application that resides in a docker container on our local machine. At the end of it, well have
- A rails app with a postgresql Database
- No local ruby, rails or postgresql dependancies on our machine. Everything will be dockerized.
Pre-requisites
- The command
docker -v
should work - The command
docker-compose -v
should work - The command
git --version
should work - Refer to the installation docs to upgrade if neccesary.
The Rails Docker image
In order to start developing, we need a docker container with the rails environment. We’ll build an image for that. Docker keeps track of your image as you make edits in the docker cache. In order to track, we need the main files that manage the rails application as well as the main docker files. First thing you need according to docker compose are the following files
Dockerfile
using whatever is the latest version of ruby on alpine. We use alpine because we want the smallest footprint possible for out app. If you change to another distro like ubuntu, then make sure you use the appropriate package manager e.g. apt-get instead of apkGemfile
with rails declarationGemfile.lock
that will be blankdocker-compose
to make running docker commands easier.dockerignore
file to exclude certain files from the build. This helps keep the image small.
These four files need to reside in the folder where our app will be. Details of what the commands do are included in the comments alongside the commands.
# Dockerfile
FROM ruby:2.5.1-alpine
LABEL maintainer="Kariuki Gathitu <kgathi2@gmail.com>"
LABEL version="1.0"
# Packages needed to get Rails running in Alpine.
# DB_PACKAGES="sqlite-dev postgresql-dev mysql-dev" \
ENV BUILD_PACKAGES="curl-dev ruby-dev build-base bash" \
DEV_PACKAGES="zlib-dev libxml2-dev libxslt-dev tzdata yaml-dev" \
DB_PACKAGES="postgresql-dev postgresql-client" \
RUBY_PACKAGES="ruby-json yaml nodejs"
# Update and install base packages
RUN apk update && \
apk upgrade && \
apk add --update\
$BUILD_PACKAGES \
$DEV_PACKAGES \
$DB_PACKAGES \
$RUBY_PACKAGES && \
rm -rf /var/cache/apk/* && \
mkdir -p /usr/src/app
# Create system user to run as non-root.
RUN addgroup -S admin -g 1000 && adduser -S -g '' -u 1000 -G admin deploy
# Set the Rails Environment Variables for production
ENV RAILS_ROOT /home/deploy/app
ENV RAILS_LOG_TO_STDOUT 1
ENV RAILS_ENV production
# Set user as deploy from here on out
USER deploy
# Configure the main working directory. This is the base
# directory used in any further RUN, COPY, and ENTRYPOINT
# commands.
RUN mkdir -p $RAILS_ROOT
WORKDIR $RAILS_ROOT
# Copy the Gemfile as well as the Gemfile.lock and install
# the RubyGems. This is a separate step so the dependencies
# will be cached unless changes to one of those two files
# are made.
COPY --chown=deploy:admin Gemfile Gemfile.lock ./
RUN gem install bundler
RUN bundle install --jobs 20 --retry 5
# Copy the main application.
COPY --chown=deploy:admin . ./
# Expose the applications port to the host machine
EXPOSE 3000
# Command to run when the container is started.
CMD ["puma", "-C", "config/puma.rb"]
Dockerfile
We run the Dockerfile installation commands as a new deploy
user, which is the recommended practice. And when we copy files across, in order to avoid permission issues, we make sure the copy user is the non-root user we created.
# Gemfile
source 'https://rubygems.org'
gem 'rails'
Gemfile
# docker-compose.yml
version: '3'
services:
app:
build: .
command: 'puma -C config/puma.rb'
stdin_open: true
tty: true
environment:
- RAILS_ENV=development
ports:
- '80:3000'
links:
- postgres
volumes:
- .:/home/deploy/app
postgres:
image: postgres:9.6.2-alpine
environment:
- POSTGRES_PASSWORD=mysecretpassword
ports:
- '5432'
volumes:
- data:/var/lib/postgresql/data
volumes:
data:
docker-compose.yml
Mapping volumes in the app service, .:/home/deploy/app
only happens when running with docker compose. This is so that the developer can have realtime interaction with the app in the container when developing. Otherwise, you’d need to restart the container everytime you made changes. This however does not extend to the initial files we had, mainly the Gemfile
. Any Change to the Gemfile
or Dockerfile
will require a rebuild of the container. We have also used a different service for the database. Postgres will reside in its own container. We are also binding the container port 3000
to our host machines (your laptop) port 80
.
# .dockerignore
**/.git*
**/*.sqlite3
**/*.sqlite3-journal
log/*
tmp/*
storage/*
**/README.md
secrets/*
**/docker-compose.yml
**/Dockerfile
.dockerignore
Rails App
1. Generate New Rails app
Now that we have our configuration done, all that is left is to generate the rails app using docker compose
docker-compose run --no-deps app rails new . --force --database=postgresql
# For API only app
docker-compose run --no-deps app rails new . --force --database=postgresql --api
#then rebuild image due to new Gemfile
docker-compose build
This generates a new rails boilerplate in the current directory. Because our volumes are mapped, anything happening in the container is reflected on the host machine, so our directory will now have a brand new rails application. The --no-deps
flag tells compose not to start dependent services, in this case the postgres service.
2. Connect the database
We have the postgres DB in its own container with the official image from docker hub. We need to point our app to this database instance.
# app/config/database.yml
default: &default
adapter: postgresql
encoding: unicode
pool: 5
# Database credentials
host: postgres # name for db service in docker-compose.yml
username: postgres # default user for postgresql docker image
password: mysecretpassword # must match POSTGRES_PASSWORD in docker-compose.yml
app/config/database.yml
Now start the app daemonized
docker-compose up -d
The app will start in development mode because the docker-compose.yml file overrides the env RAILS_ENV
. In another window, initialize the DB.
docker-compose exec app rails db:create
# or if the container was not yet started
docker-compose run app rails db:create
The database file are persisted in the data:
docker volume. Without it you would need to run docker-compose run web rake db:create
whenever restarting your app to recreate the database.
Your app should be available at localhost.
To stop the application run docker-compose down
.
If all went well, then we need to “save” our app in git. Stop the app and check it into git
docker-compose down
git add .
git commit -m 'Version 0.0.0 - App Initialized'
So now we have our app boilerplate ready with persistence on a separate postgresql database.
3. Code
As beautiful as the rails welcome page is, it doesn’t tell us if our environment behaves as we need it to. Let’s do some quick scaffolding to test.
First bring the app back up with docker-compose up
, then:
docker-compose exec app bundle exec rails g scaffold user username first_name last_name phone:integer
Then run the migrations:
docker-compose exec app bundle exec rails db:migrate
And edit our routes to point to our list of users:
Rails.application.routes.draw do
resources :users
root "users#index"
# For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
end
Reload the page and voila:
You can add a few users to populate the page.
Finally, lets shutdown our server and see if it comes back up with what we expect.
Hit Ctrl-C
to bring down the services and wait for them to exit. Restart with a docker-compose up
.
If everything went well you should be greeted with the list of users you created. And …, breathe!
Avoid If You Can
- Building a custom image - this is all well and good for the learning experience or when there isn’t a ready image on dockerhub. Otherwise, it’s a pain not worth having.
- Installing postgres in the rails container - two reasons why; using a separate database image means you can re-use it in a new app, and second, making postgres work in the app container is a major hustle. You’ll need to set up the postgres user, set up authentication and find a way to make sure the db service is started when you run your app.
- Bind mounts for 3rd-party application data - use docker managed volumes for data you don’t need to interact with directly. Docker sets up the volumes so that the container has the proper access rights, helping avoid a world of pain in managing file permissions.
And that’s that… finally. We have our environment set up and can finally bring that app to life! Whatever you choose to build, ganbatte.