Dockerizing a Ruby on Rails Application

What is Docker?

What Makes Docker Different from a Virtual Machine

What’s the Bottom Line?

The Benefits of Using Docker

1. Cross Environment Consistency

2. Expand Your Development Team Painlessly

3. Use Whatever Technology Fits Best

4. Build Your Image Once and Deploy It Many Times

5. Developers and Operation Managers Can Work Together

Prerequisites

Installing Docker

The Rails Application

$ git clone YOUR_REPOSITORY_URL

Generating a New Rails Application

Creating a Rails Image

# Dockerfile.rails
FROM ruby:3.1.2 AS rails-toolbox

# Default directory
ENV INSTALL_PATH /opt/app
RUN mkdir -p $INSTALL_PATH

# Install rails
RUN gem install rails bundler
#RUN chown -R user:user /opt/app
WORKDIR /opt/app

# Run a shell
CMD ["/bin/sh"]
$  docker build -t rails-toolbox -f Dockerfile.rails .

Creating the Project

$ docker run -it -v $PWD:/opt/app rails-toolbox rails new --skip-bundle drkiq
$ rm -rf drkiq/.git

Setting Up a Strong Base

Modifying the Gemfile

gem 'unicorn', '~> 6.1.0'
gem 'pg', '~> 1.3.5'
gem 'sidekiq', '~> 6.4.2'
gem 'redis-rails', '~> 5.0.2'

DRYing Out the Database Configuration

---

development:
url: <%= ENV['DATABASE_URL'].gsub('?', '_development?') %>

test:
url: <%= ENV['DATABASE_URL'].gsub('?', '_test?') %>

staging:
url: <%= ENV['DATABASE_URL'].gsub('?', '_staging?') %>

production:
url: <%= ENV['DATABASE_URL'].gsub('?', '_production?') %>

DRYing Out the Secrets File

development: &default
secret_key_base: <%= ENV['SECRET_TOKEN'] %>

test:
<<: *default

staging:
<<: *default

production:
<<: *default

Editing the Application Configuration

# ...

module Drkiq
class Application < Rails::Application
config.load_defaults 7.0

config.log_level = :debug
config.log_tags = [:subdomain, :uuid]
config.logger = ActiveSupport::TaggedLogging.new(Logger.new(STDOUT))

config.cache_store = :redis_store, ENV['CACHE_URL'],
{ namespace: 'drkiq::cache' }

config.active_job.queue_adapter = :sidekiq
end
end

Creating the Unicorn Config

# Heavily inspired by GitLab:
# https://github.com/gitlabhq/gitlabhq/blob/master/config/unicorn.rb.example

worker_processes ENV['WORKER_PROCESSES'].to_i
listen ENV['LISTEN_ON']
timeout 30
preload_app true
GC.respond_to?(:copy_on_write_friendly=) && GC.copy_on_write_friendly = true

check_client_connection false

before_fork do |server, worker|
defined?(ActiveRecord::Base) && ActiveRecord::Base.connection.disconnect!

old_pid = "#{server.config[:pid]}.oldbin"
if old_pid != server.pid
begin
sig = (worker.nr + 1) >= server.worker_processes ? :QUIT : :TTOU
Process.kill(sig, File.read(old_pid).to_i)
rescue Errno::ENOENT, Errno::ESRCH
end
end
end

after_fork do |server, worker|
defined?(ActiveRecord::Base) && ActiveRecord::Base.establish_connection
end

Creating the Sidekiq Initialize Config

sidekiq_config = { url: ENV['JOB_WORKER_URL'] }

Sidekiq.configure_server do |config|
config.redis = sidekiq_config
end

Sidekiq.configure_client do |config|
config.redis = sidekiq_config
end
config.hosts << "drkiq"
$ cd ..
$ touch env-example
# You would typically use rake secret to generate a secure token. It is
# critical that you keep this value private in production.
SECRET_TOKEN=Wa4Kdu6hMt3tYKm4jb9p4vZUuc7jBVFw

WORKER_PROCESSES=1
LISTEN_ON=0.0.0.0:8010
DATABASE_URL=postgresql://drkiq:test_db_password@postgres:5432/drkiq?encoding=utf8&pool=5&timeout=5000
CACHE_URL=redis://redis:6379/0
JOB_WORKER_URL=redis://redis:6379/0
$ cp env-example .env
$ echo ".env" >> .gitignore

Dockerizing Your Rails Application

# Dockerfile development version
FROM ruby:3.1.2 AS drkiq-development

# Install yarn
RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg -o /root/yarn-pubkey.gpg && apt-key add /root/yarn-pubkey.gpg
RUN echo "deb https://dl.yarnpkg.com/debian/ stable main" > /etc/apt/sources.list.d/yarn.list
RUN apt-get update && apt-get install -y --no-install-recommends nodejs yarn

# Default directory
ENV INSTALL_PATH /opt/app
RUN mkdir -p $INSTALL_PATH

# Install gems
WORKDIR $INSTALL_PATH
COPY drkiq/ .
RUN rm -rf node_modules vendor
RUN gem install rails bundler
RUN bundle install
RUN yarn install

# Start server
CMD bundle exec unicorn -c config/unicorn.rb

Configuring Ngnix

# reverse-proxy.conf

server {
listen 8020;
server_name example.org;

location / {
proxy_pass http://drkiq:8010;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
# Dockerfile.nginx

FROM nginx:latest
COPY reverse-proxy.conf /etc/nginx/conf.d/reverse-proxy.conf
EXPOSE 8020
STOPSIGNAL SIGTERM
CMD ["nginx", "-g", "daemon off;"]

Creating a dockerignore File

.git
.dockerignore
.env
drkiq/node_modules/
drkiq/vendor/bundle/
drkiq/tmp/

What is Docker Compose?

Creating the Docker Compose Configuration File

version: "3.9"

services:

postgres:
image: postgres:14.2
environment:
POSTGRES_USER: drkiq
POSTGRES_PASSWORD: test_db_password
ports:
- '5432:5432'
volumes:
- drkiq-postgres:/var/lib/postgresql/data

redis:
image: redis:7.0
ports:
- '6379:6379'
volumes:
- drkiq-redis:/var/lib/redis/data

drkiq:
build:
context: .
volumes:
- ./drkiq:/opt/app
links:
- postgres
- redis
ports:
- '8010:8010'
env_file:
- .env

sidekiq:
build:
context: .
command: bundle exec sidekiq
links:
- postgres
- redis
env_file:
- .env

nginx:
build:
context: .
dockerfile: ./Dockerfile.nginx
links:
- drkiq
ports:
- '8020:8020'

volumes:
drkiq-postgres:
drkiq-redis:

Creating the Volumes

$ docker volume create --name drkiq-postgres
$ docker volume create --name drkiq-redis

Running Everything

$ docker compose up --build
postgres_1  | ...
redis_1 | ...
drkiq_1 | ...
sidekiq_1 | ...
nginx_1 | ...

Initialize the Database

$ docker­ compose run drkiq rake db:reset
$ docker­ compose run drkiq rake db:migrate

Running Everything, Round 2

$ docker compose up

Testing It Out

Working with the Rails Application

Generating a Controller

docker compose run drkiq rails g controller Pages home

Modify the Routes File

root 'pages#home'

Adding a New Job

docker compose run drkiq rails g job counter

Modifying the Counter Job

def perform(*args)
21 + 21
end

Modifying the Pages Controller

def home
@meaning_of_life = CounterJob.perform_now
end

Modifying the Home View

<h1>The meaning of life is <%= @meaning_of_life %></h1>

Restart the Rails Application

Experimenting on Your Own

Adding Some Tests

require 'test_helper'

class CounterJobTest < ActiveJob::TestCase
test "returns 42" do
assert_equal 42, CounterJob.perform_now
end
end
require 'test_helper'

class PagesControllerTest < ActionDispatch::IntegrationTest
test "should get home" do
get "/"
assert_response :success
end
end
$ docker compose run drkiq rake db:test:prepare
$ docker compose run drkiq rails test

...
Finished in 4.850950s, 0.4123 runs/s, 0.4123 assertions/s.
2 runs, 2 assertions, 0 failures, 0 errors, 0 skips
$ git add -A 
$ git commit -m "initial commit"
$ git push origin master

Continuous Integration for dockerizing Ruby

Prerequisites

Production Images

# Dockerfile CI version
FROM registry.semaphoreci.com/ruby:3.1

# Default directory
ENV INSTALL_PATH /opt/app
RUN mkdir -p $INSTALL_PATH

# Install Nodejs
RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg -o /root/yarn-pubkey.gpg && apt-key add /root/yarn-pubkey.gpg
RUN echo "deb https://dl.yarnpkg.com/debian/ stable main" > /etc/apt/sources.list.d/yarn.list
RUN apt-get update && apt-get install -y --no-install-recommends nodejs yarn

ENV INSTALL_PATH /opt/app
RUN mkdir -p $INSTALL_PATH

# Install gems
WORKDIR $INSTALL_PATH
COPY drkiq/ .
RUN rm -rf node_modules vendor
RUN gem install rails bundler
RUN bundle install
RUN yarn install

CMD bundle exec unicorn -c config/unicorn.rb
$ git add Dockerfile.production
$ git commit -m "add dockerfile"
$ git push origin master

Continuous Integration Pipeline

Build Block

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store