17 March 2017
I will explain why Docker is a good choice for development of Rails applications. Next, I will give a step-by-step walkthrough of adding Docker to a fairly standard Rails application.
This content is based on a presentation that I gave at WellRailed, a Wellington, New Zealand based Ruby on Rails meetup in December 2016.
Docker is a platform that allows you to run containers. These containers, when compared to traditional VMs, are a much faster and more efficient mechanism for isolating the different parts of your application from each other.
This image from IBM gives a good comparison:
A fast start from ZERO in 3 steps:
And when finished development:
First off, we should itemise the dependencies in this application:
Before starting, you should download Docker for your OS. It is free and called Docker Community Edition these days.
Docker requires us to define the different dependencies on the application, and this is done in the docker-compose.yml
file. This file lives in the root directory of your Rails application.
For our Postgresql dependency, we will define the following
image — this the container definition that is downloaded from https://hub.docker.com/
container — this can be thought of as the VM that runs, and is based on the downloaded image.
volume — this is the persistent storage which Postgresql will store the data files.
Here is the Postgresql section within the new docker-compose.yml
file:
version: '3'
services:
postgresql:
image: postgres:9.4.1
ports:
- "5432:5432"
volumes:
- postgresql-data:/var/lib/postgresql/data
volumes:
postgresql-data: {}
With the docker-compose.yml
file saved, we can start up the database by issuing this command:
docker-compose up
See if it works by adjusting the database.yml
file:
development:
adapter: postgresql
encoding: unicode
pool: 5
database: adjuster_development
url: <%= ENV['POSTGRESQL_URL'] || 'postgresql://postgres@localhost:5432' %>
test:
adapter: postgresql
encoding: unicode
pool: 5
database: adjuster_test
url: <%= ENV['POSTGRESQL_URL'] || 'postgresql://postgres@localhost:5432' %>
Next, try creating the database:
rake db:create
Next, run a few tests to see it working:
rake
............................................FFFFFFF............................
............
This fails due to the missing Redis container. Let’s fix that
Add to docker-compose.yml
file:
redis:
image: redis:2.8
ports:
- "6379:6379"
volumes:
- redis-data:/var/lib/redis/datavolumes:
redis-data: {}
Then restart docker-compose:
docker-compose down; docker-compose up
Update Rails application sidekiq.rb
to use the new Redis container:
Sidekiq.configure_server do |config|
config.redis = {
url: "redis://#{ENV['REDIS_HOST' || '127.0.0.1']}:#{ENV['REDIS_HOST']}/12"
}
endSidekiq.configure_client do |config|
config.redis = {
url: "redis://#{ENV['REDIS_HOST' || '127.0.0.1']}:#{ENV['REDIS_HOST']}/12"
}
end
Run the tests a second time:
rake
...............................................................................
..............................
Win!
We’ve been running Rails from the host operating system, in my case Mac OSX. Now we want to try getting Rails running within a new web container, as defined in the docker-compose.yml file.
Make the following additions:
web:
build: .
ports:
- "3000:3000"
volumes:
- .:/app
- bundle-cache:/bundle
depends_on:
- redis
- postgresql
command: bin/docker_web
volumes:
bundle-cache: {}
Add a .env
file:
POSTGRESQL_URL=postgresql://postgres@postgresql:5432
REDIS_ADDRESS=redis
Add the new .env
file to your .gitignore
file.
Next, add a Dockerfile
that describes the dependencies and installation steps of your Rails app:
FROM ruby:2.2.1
# Set an environment variable to store where the app is installed to
# inside of the Docker image.
ENV BUNDLE_PATH /bundle
ENV LANG C.UTF-8
ENV INSTALL_PATH /app
RUN echo "deb http://apt.postgresql.org/pub/repos/apt/ trusty-pgdg main" > /etc/apt/sources.list.d/pgdg.list
RUN apt-key adv --keyserver hkp://p80.pool.sks-keyservers.net:80 --recv-keys B97B0AFCAA1A47F044F244A07FCC7D46ACCC4CF8
# Install dependencies:
# - build-essential: To ensure certain gems can be compiled
# - bundler: ensure most recent version is installed
# - nodejs: Compile assets
RUN apt-get update && apt-get install -qq -y build-essential nodejs postgresql-client-9.5 --fix-missing --no-install-recommends
RUN gem install bundler
RUN curl -sL https://deb.nodesource.com/setup_6.x | bash
RUN apt-get install -qq -y nodejs
# This sets the context of where commands will be ran in and is
# documented on Docker's website extensively.
RUN mkdir -p $INSTALL_PATH
WORKDIR $INSTALL_PATH
ADD . $INSTALL_PATH
Next, add the launch script in bin/docker_web
#! /bin/bash
export RAILS_ENV=development
bundle check || bundle install — jobs=10
# Initialise development database
FILE=$INSTALL_PATH/tmp/database_initialised_development.txt
if [ ! -f "$FILE" ]; then
echo "Creating and loading development databases"
RAILS_ENV=development bundle exec rake db:setup
touch "$FILE"
fi
# Initialise test database
FILE=$INSTALL_PATH/tmp/database_initialised_test.txt
if [ ! -f "$FILE" ]; then
echo "Creating and loading test databases"
RAILS_ENV=test bundle exec rake db:create db:schema:load
touch "$FILE"
fi
echo "Migrating and refreshing reference data"
bundle exec rake db:migrate
RAILS_ENV=test bundle exec rake db:migrate
npm install
rm -f tmp/pids/server.pid
bundle exec rails s -p 3000 -b 0.0.0.0
Don’t forget to make this executable:
chmod +x bin/docker_web
Time to try out the application:
docker-compose down
docker-compose up
open http://0.0.0.0:3000/
With a little luck, the application should be running.
Next step is to add the Sidekiq worker. Add the configuration to the docker-compose.yml
file:
sidekiq:
build: .
volumes:
- .:/app
- bundle-cache:/bundle
depends_on:
- redis
- postgresql
command: bin/docker_sidekiq
Add the Sidekiq boot script in bin/docker_sidekiq
#! /bin/bash
export RAILS_ENV=development
bundle check
if (($? > 0)); then
echo '***********************************************************'
echo 'Await web container to install updated gems, then restart Docker'
echo '***********************************************************'
exit 1
fi
bundle exec sidekiq
Don’t forget to make this executable:
chmod +x bin/docker_sidekiq
Time to try out the application again:
docker-compose down
docker-compose up
You should see the Sidekiq application also startup and connect to Redis.
Docker can be a little slow on OSX. I believe this is related to how the file system is mounted within the Docker containers.
What you can do, is use Docker to run all of the non-web portions of your application. Then start Rails within the host operating system. We will often run our specs within the host OS too.
If you need to do some debugging within your Rails application inside of the container, you can use the pry-remote gem, and then when you hit a breakpoint:
docker-compose exec web bash
pry-remote
Occasionally, you may need to run a few different Rails applications at the same time. For example, when debugging an data sharing fault on the Addressfinder system, we might want both the Addressfinder API application and the Addressfinder Portal application running together on the same laptop.
This can pose a challenge, as both apps may be configured to expose their ports on 127.0.0.1
and you will end up with a clash.
On solution is to attach all the ports for App #1 to 127.0.0.1
and the ports for App #2 to 127.0.0.2
. To achieve this, you’ll first need to have create the 127.0.0.2
address as a loopback alias. We have written a tiny gem that will automate this — it’s called the loopback_alias gem.
You would then update your docker-compose.yml
file to refer to the new IP address:
postgresql:
image: postgres:9.4.1
ports:
- "$HOST_IP:5432:5432"
volumes:
- postgresql-data:/var/lib/postgresql/data
and add this line to your .env
file:
HOST_IP=127.0.0.2
We make good use of the Mailcatcher gem in development, and it is very easy to drop this into your stack. Just add this to your docker-compose.yml
.
mailcatcher:
image: schickling/mailcatcher
ports:
- "1080:1080"
- "1025:1025"
and the following to your development.rb
file:
config.action_mailer.delivery_method = :smtp
config.action_mailer.smtp_settings = {
address: ENV['SMTP_ADDRESS'] || '127.0.0.1'
port: ENV['SMTP_PORT'] || 1025
}
We’ve defined Docker configuration for all of our static websites that are generated with the Middleman gem.
If you are declaring some background tasks in your Procfile
for running in production
, then these processes could also be added as Docker containers for running in development
. You would do this in a similar manner to how the Sidekiq process is configured (above).
We’ve found adding Docker to be the biggest productivity gain we’ve had for some time at Abletech. Our team are able to quickly get going on new projects, and we know we’re all developing within a consistent environment.
Team members, of varying experience with infrastructure, have been able to checkout Docker-based applications and become productive without needing someone to help.
This year, we look forward to exploring the possibilities of using Docker in production
as tools such as Docker Swam and Kubernetes become more mature.