Docker for Rails Developers

by msypniewski511 in Docker

Zaczynamy

Running a Ruby Script Without Ruby Installed

docker run ruby:3.0.0

Generating a New Rails App

$ ​ docker​​ ​ run​​ ​ -i​​ ​ -t​​ ​ --rm​​ ​ -v​​ ​ ${PWD}:/usr/src/app​​ ​ ruby:3.2.2 bash
> root@b6bfd08833a4:/# 

root@b6bfd08833a4:/# cd /usr/src/app
root@b6bfd08833a4:/# gem install rails
root@b6bfd08833a4:/usr/src/app# rails new myapp --skip-test --skip-bundle
root@b6bfd08833a4:/usr/src/app# exit

$ sudo chown $(id -u):$(id -g) -R .

For rails 6

Skomentoj "gem 'webpacker'"

Running a Rails App in a Container

Create Dockerfile:

FROM ruby:3.2.2

RUN apt-get update -yqq
RUN apt-get install -yqq --no-install-recommends nodejs

COPY . /usr/src/app

WORKDIR /usr/src/app
RUN bundle install

Build Image

$ ​ docker​​ ​ build​​ ​ [options]​​ ​ path/to/build/directory
for example:
$ ​ docker​​ ​ build​​ ​ .

Running a Rails Server

$ docker run -p 3000:3000 image_id bin/rails s -b 0.0.0.0

Naming and Versioning our Image

$ docker tag 848010a87d2b railsapp

$ docker tag 848010a87d2b railsapp:1.0

A Default Command

Dockerfile

FROM ruby:3.2.2

RUN apt-get update -yqq
RUN apt-get install -yqq --no-install-recommends nodejs

COPY . /usr/src/app

WORKDIR /usr/src/app
RUN bundle install

CMD ["bin/rails",  "s",  "-b",  "0.0.0.0"]
$ docker​​ ​ build​​ ​ -t​​ ​ railsapp​​ ​ .

Now we can start server:

$ docker run -p localhost_port:container_port container_name

Dockerignore

Create .dockerignore

/# Git
.git
.gitignore

/# Logs
log/*

/# Temp files
tmp/*

/# Editor temp files
*.swp
*.swo

Rebuild image

$ docker build -t railsapp .

The Image Build Cache

Updating packages

Dockerfile

RUN ​ apt-get update -yqq && apt-get install --yqq --no-install-recommends ​ \
nodejs ​ \
vim

Rebuild

$ docker build -t railsapp .

Cashed gemfile.

Dockerfile

FROM ruby:3.2.2

RUN apt-get update -yqq && apt-get install --yqq -no-install-recommends \
  nodejs \
  vim

- COPY .  /usr/src/app/

+ COPY Gemfile* /usr/src/app/
WORKDIR /usr/src/app
RUN bundle install

+ COPY . /usr/src/app/

CMD ["bin/rails",  "s",  "-b",  "0.0.0.0"]

Rebuild

$ docker build -t railsapp .

Labeled

LABEL <key>=<value>
for example:
LABEL maintainer="msypniewski511@gmail.com"

Docker Compose

Create docker-compose.yml file.

services:

  web:
    build: .
    ports:
      - "4000:3000"

Lunch app

$ docker-compose up

Mounting a local volume
docker-comose.yml

services:

  web:
    build: ​ .
    ports:
      - ​ " ​4000:3000"
    volumes:
      - ​ .:/usr/src/app

Relaunch:

$ docker-compose up -d

Adding Redis

In docker-compose.yml file

services:

  web:
    build: ​ .
    ports:
      - ​ " ​4000:3000"
    volumes:
      - ​ .:/usr/src/app

+ redis:
+   image: redis

Start Redis server

$ docker-compose up -d redis

Connect to Redis

$ docker-compose run -rm redis redis-cli -h redis

Connect app to redois

$ docker-compose stop web

Add redis gem in Gemfile

gem ​ 'redis'​ , ​ '~> 4.0'
$ docker-compose build web

Starting all

$ docker-compose stop

$ docker-compose up -d

Adding Postgres

Add in docker-compose.yml

services:
  web:
    build: ​ .
    ports:
      - ​ "​4000:3000"
    volumes:
      - ​ .:/usr/src/app
  redis:
    image: ​ redis
+  database:
+    image: ​ postgres
+    environment:
+     POSTGRES_USER: ​ postgres
+     POSTGRES_PASSWORD: ​ some-long-secure-password
+     POSTGRES_DB: ​ myapp_development

Start postgres

$ docker-compose​​ ​ up​​ ​ -d​​ ​ database

Connecting Rails App to Postgres

  • install pg gem

in Gemfile file change gem 'sql' to gem 'pg', '~>1.0'

  • rebuild image

rebuild image

  $ docker-comose stop web
  $ docker-compose build web
  • change config/database.yml
  default: &default
    adapter: PostgreSQL
    encoding: unicode
    host: <%= ENV.fetch('DATABASE_HOST') %>
    username: <%= ENV.fetch('POSTGRES_USER') %>
    password: <%= ENV.fetch('POSTGRES_PASSWORD') %>
    database: <%= ENV.fetch('POSTGRES_DB') %>
    pool: 5
    variables:
      statement_timeout: 5000
  development:
    <<: *default
  test:
    <<: *default
    database: myapp_test
  production:
    <<: *default
  • set environment variables for web and database service.
$ mkdir -p .env/development

Create web and database files.
web file

DATABASE_HOST=database

database file

POSTGRES_USER=postgres
POSTGRES_PASSWORD=postgres
POSTGRES_DB=myapp_development
  • change docker-compose.yml file
services:
  web:
    build: ​ .
    ports:
      - ​ "​4000:3000"
    volumes:
      - ​ .:/usr/src/app
    env_file
      - .env/development/database
      - .env/development/web
  redis:
    image: ​ redis
  database:
    image: ​ postgres
    env_file:
     - .env/development/database
  • create databases
$ docker-compose run --rm web rails db:create
  • restarting web service
$ docker-compose up -d --force-recreate web
  • example
$ docker-compose exec web rails g scaffold User first_name:string last_name:string
$ sudo chown <your_user>:<your_group> -R .
$ docker-compose exec web rails db:migrate

Decoupling data from the container

  • add named volume to docker-compose.yml
services:
  web:
    build: ​ .
    ports:
      - ​ "​4000:3000"
    volumes:
      - ​ .:/usr/src/app
    env_file
      - .env/development/database
      - .env/development/web
  redis:
    image: ​ redis
  database:
    image: ​ postgres
    env_file:
     - .env/development/database
    volumes:
      - db_data:/var/lib/postgresql/data
volumes:
  db_data:
  • restart and recreate database service
$ docker-compose stop database
$ docker-compose rm -f database
$ docker-compose up -d database
Recreate database in service
$ docker-compose exec web rails db:create db:migrarate

To check where actually is stored our data:

$ docker volume inspect --format '{{ 'Mountpoint' }} myapp_db_data

Testing

RSpec

  • In Gemfile file
group :development, :test do
  # Call 'byebug' anywhere in the code to stop execution and get a debugger console
  gem 'byebug', platforms: [:mri, :mingw, :x64_mingw]
  gem 'rspec-rails', '~> 6.0.0'
end
$ docker-compose stop web
$ docker-compose build web
$ docker-compose up -d --force-recreate web
# install RSpec
$ docker-compose exec web rails generate rspec:install
 # check installation
$ docker-compose exec web rspec

System Tests

1. Capybara

In Gemfile add capybara gem

group :development, :test do
  # Call 'byebug' anywhere in the code to stop execution and get a debugger console
  gem 'byebug', platforms: [:mri, :mingw, :x64_mingw]
  gem 'rspec-rails', '~> 6.0.0'
  gem 'capybara'
end

Rebuild image:

$ docker-compose build web
$ docker-compose stop web
$ docker-compose up -d --force-recreate web

Create a folder for system/feature tests

$ mkdir spec/system

Edit spec/rails_helper.rb

# This file is copied to spec/ when you run 'rails generate rspec:install'
require 'spec_helper'
ENV['RAILS_ENV'] ||= 'test'
require_relative '../config/environment'
# Prevent database truncation if the environment is production
abort("The Rails environment is running in production mode!") if Rails.env.production?
require 'rspec/rails'
require 'capybara/rails'
require 'capybara/rspec'
# Add additional requires below this line. Rails is not loaded until this point!

# Requires supporting ruby files with custom matchers and macros, etc, in
# spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are
# run as spec files by default. This means that files in spec/support that end
# in _spec.rb will both be required and run as specs, causing the specs to be
# run twice. It is recommended that you do not name files matching this glob to
# end with _spec.rb. You can configure this pattern with the --pattern
# option on the command line or in ~/.rspec, .rspec or `.rspec-local`.
#
# The following line is provided for convenience purposes. It has the downside
# of increasing the boot-up time by auto-requiring all files in the support
# directory. Alternatively, in the individual `*_spec.rb` files, manually
# require only the support files necessary.
#
# Dir[Rails.root.join('spec', 'support', '**', '*.rb')].sort.each { |f| require f }

# Checks for pending migrations and applies them before tests are run.
# If you are not using ActiveRecord, you can remove these lines.
begin
  ActiveRecord::Migration.maintain_test_schema!
rescue ActiveRecord::PendingMigrationError => e
  abort e.to_s.strip
end
RSpec.configure do |config|
  # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures
  config.fixture_path = "#{::Rails.root}/spec/fixtures"

  # If you're not using ActiveRecord, or you'd prefer not to run each of your
  # examples within a transaction, remove the following line or assign false
  # instead of true.
  config.use_transactional_fixtures = true

  # You can uncomment this line to turn off ActiveRecord support entirely.
  # config.use_active_record = false

  # RSpec Rails can automatically mix in different behaviours to your tests
  # based on their file location, for example enabling you to call `get` and
  # `post` in specs under `spec/controllers`.
  #
  # You can disable this behaviour by removing the line below, and instead
  # explicitly tag your specs with their type, e.g.:
  #
  #     RSpec.describe UsersController, type: :controller do
  #       
  #     end
  #
  # The different available types are documented in the features, such as in
  # https://rspec.info/features/6-0/rspec-rails
  config.infer_spec_type_from_file_location!

  # Filter lines from Rails gems in backtraces.
  config.filter_rails_from_backtrace!
  # arbitrary gems may also be filtered via:
  # config.filter_gems_from_backtrace("gem name")
  config.before( :each, type: :system) do
    driven_by :rack_test
  end
end
# check installation
$ docker-compose exec web rspec spec/system

2. JavaScript

In Gemfile file add selenium-webdriver gem:

group :development, :test do
  # Call 'byebug' anywhere in the code to stop execution and get a debugger console
  gem 'byebug', platforms: [:mri, :mingw, :x64_mingw]
  gem 'rspec-rails', '~> 6.0.0'
  gem 'capybara'
  gem 'selenium-webdriver', '~> 4.4'
end

Rebuild image:

$ docker-compose build web
$ docker-compose stop web
$ docker-compose up -d --force-recreate web

Edit docker-compose.yml file:

selenium_chrome:
  image: selenium/standalone-chrome
  logging:
    driver: none
  ports:
    - "5900:4444"

Start selenium service:

$ docker-compose up -d selenium_chrome

Configure Capybara to use Chrome spec/support/capybara.rb:

Capybara.register_driver :selenium_chrome_in_container do |app|
  Capybara::Selenium::Driver.new(
    app, 
    browser: :remote, 
    options: Selenium::WebDriver::Options.chrome, 
    url: 'http://selenium_chrome:4444/wd/hub'
  )
end

Configuring RSpec System Tests - spec/rails_helper.rb:

# This file is copied to spec/ when you run 'rails generate rspec:install'
require 'spec_helper'
ENV['RAILS_ENV'] ||= 'test'
require_relative '../config/environment'
# Prevent database truncation if the environment is production
abort("The Rails environment is running in production mode!") if Rails.env.production?
require 'rspec/rails'
require 'capybara/rails'
require 'capybara/rspec'
require_relative './support/capybara.rb'
# Add additional requires below this line. Rails is not loaded until this point!

# Requires supporting ruby files with custom matchers and macros, etc, in
# spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are
# run as spec files by default. This means that files in spec/support that end
# in _spec.rb will both be required and run as specs, causing the specs to be
# run twice. It is recommended that you do not name files matching this glob to
# end with _spec.rb. You can configure this pattern with the --pattern
# option on the command line or in ~/.rspec, .rspec or `.rspec-local`.
#
# The following line is provided for convenience purposes. It has the downside
# of increasing the boot-up time by auto-requiring all files in the support
# directory. Alternatively, in the individual `*_spec.rb` files, manually
# require only the support files necessary.
#
# Dir[Rails.root.join('spec', 'support', '**', '*.rb')].sort.each { |f| require f }

# Checks for pending migrations and applies them before tests are run.
# If you are not using ActiveRecord, you can remove these lines.
begin
  ActiveRecord::Migration.maintain_test_schema!
rescue ActiveRecord::PendingMigrationError => e
  abort e.to_s.strip
end
RSpec.configure do |config|
  # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures
  config.fixture_path = "#{::Rails.root}/spec/fixtures"

  # If you're not using ActiveRecord, or you'd prefer not to run each of your
  # examples within a transaction, remove the following line or assign false
  # instead of true.
  config.use_transactional_fixtures = true

  # You can uncomment this line to turn off ActiveRecord support entirely.
  # config.use_active_record = false

  # RSpec Rails can automatically mix in different behaviours to your tests
  # based on their file location, for example enabling you to call `get` and
  # `post` in specs under `spec/controllers`.
  #
  # You can disable this behaviour by removing the line below, and instead
  # explicitly tag your specs with their type, e.g.:
  #
  #     RSpec.describe UsersController, type: :controller do
  #       
  #     end
  #
  # The different available types are documented in the features, such as in
  # https://rspec.info/features/6-0/rspec-rails
  config.infer_spec_type_from_file_location!

  # Filter lines from Rails gems in backtraces.
  config.filter_rails_from_backtrace!
  # arbitrary gems may also be filtered via:
  # config.filter_gems_from_backtrace("gem name")
  config.before( :each, type: :system) do
    driven_by :rack_test
  end
  config.before(:each, type: :system, js: true) do
    driven_by :selenium_chrome_in_container
    Capybara.server_host = "0.0.0.0"
    Capybara.server_port = 4002
    Capybara.app_host = 'http://web:4002'
  end
end

Update web services with new ports:

  web:
    build: .
    ports:
      - "4444:3000"
      - "4001:4002"
# Pick up change
$ docker-compose stop web
$ docker-compose up -d --force-recreate web

1 Replies


Leave a replay

To replay you need to login. Don't have an account? Sign up for one.