Dockerizing a Ruby on Rails Application

  • You will have a basic idea of what Docker is.
  • How Docker can help you to streamline development.
  • How you can use Continuous Integration and Delivery (CI/CD) to build and test your Ruby projects.

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

Create a Repository

  • Follow the instructions to create a repo.
  • Set the language to Rails:
  • Create the repository.
  • Clone it to your machine:
$ 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"]
  • FROM: defines what image to start from. We’ll use the official Ruby image as a starting point.
  • ARG: specifies build-time argument variables. If your workstation is running Linux, the user and group ids should match between the host and the docker container.
  • RUN: executes commands inside the container. In the example, we use it to create a user and group and then to install the Rails gems.
  • ENV: defines environment variables.
  • WORKDIR: changes the current directory inside the container.
  • USER: changes the active user inside the container.
  • CMD: defines the program to run when the container starts.
$  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
  • -it: attaches your terminal process with the container.
  • -v $PWD:/opt/app: binds your host machine current directory to the container, so files created inside are visible in your machine
  • rails new --skip-bundle drkiq: That’s the command we’re passing to the Rails image. It creates a new project called “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

Whitelist Docker Host

config.hosts << "drkiq"

Creating the Environment Variable File

$ 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
  • Node and Yarn
  • Rails
  • Gems in the Gemfile

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:
  • Postgres and Redis use Docker volumes to manage persistence
  • Postgres, Redis and Drkiq all expose a port
  • Drkiq and Sidekiq both have links to Postgres and Redis.
  • Drkiq and Sidekiq both read in environment variables from .env
  • Sidekiq overwrites the default CMD to run Sidekiq instead of Unicorn.

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

  • Changing the h1 color to something other than black
  • Generating a model and then running a migration
  • Adding a new action and route to the application

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

  • Docker Hub: create a free account using the Get Started button. Docker Hub provides unlimited public repositories for free.
  • Semaphore: head to Semaphore and sign up using the Sign up with GitHub button. Use your GitHub account to log in.
  1. Go to your Semaphore account menu and select Settings
  2. Click on Secrets and then Create New Secret.
  3. Create a secret called “dockerhub” with the following details:
  • DOCKER_USERNAME: your Docker Hub username.
  • DOCKER_PASSWORD: type your Docker Hub password.
  1. Click on Save Secret.

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

  • Open your Semaphore account.
  • On the left navigation menu, click on the + (plus sign) next to Projects:
  • Find your repository and click on Choose:
  • Select the Docker starter workflow. Click on Customize it first:
  • Pipeline: A pipeline has a specific objective, e.g. building. Pipelines are made of blocks that are executed from left to right in an agent.
  • Agent: The agent is the virtual machine that powers the pipeline. We have three machine types to choose from. The machine runs an optimized Ubuntu 18.04 image with build tools for many languages.
  • Block: blocks group jobs that can be executed in parallel. Jobs in a block usually have similar commands and configurations. Once all jobs in a block complete, the next block begins.
  • Job: jobs define the commands that do the work. They inherit their configuration from their parent block.

Build Block

--

--

--

Supporting developers with insights and tutorials on delivering good software. · https://semaphoreci.com

Love podcasts or audiobooks? Learn on the go with our new app.

Recommended from Medium

IPv6+Ruby part 3: Dual stack (IPv4+IPv6) TCP sockets

Cloud Pricing Comparison: AWS vs. Azure vs. Google Cloud Platform in 2022

Takeaways from maintaining an open source project for a month

RISC vs CISC Microprocessor Philosophy in 2022

How to Get Past the 15 Minute Delay Limit in Amazon SQS

Remote Testing Can Improve Your Testing Process

SRE Doesn’t Scale

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
Semaphore

Semaphore

Supporting developers with insights and tutorials on delivering good software. · https://semaphoreci.com

More from Medium

Rails User Session Handling using Redis

Synchronous Messaging using RabbitMQ and Rails

How to Add JWT Auth API to Existing Rails Application

Server with wires

Accessing a PostgreSQL database from a Ruby on Rails project with TablePlus