dockerでrails環境構築
rails5
+ mysql
の開発環境が必要になったので、dockerコンテナとして構築します。
docker-machine
で作業するにあたってものすごくハマったのでその作業記録も。
作業環境
rails
とmysql
はそれぞれ独立したコンテナにしたかったのでdocker-compose
でまとめます。
またホストマシンはmacなのでdocker-machine
でdocker
の環境を作っています。
- docker-machine v0.7.0
- docker v1.11.0
- docker-compose v1.7.0
railsコンテナ作成
方針
- ホスト側に
ruby
がインストールされていなくても開発出来るようにしたい。 - railsのアプリケーションのフォルダ自体はボリュームでマウントして、ファイルの編集自体はホスト環境で行えるようにしたい。
railsコンテナの構成
- ruby 2.3.0
- rails5 beta3
の環境で作成します。
Dockerfile
- ディレクトリ構成
% tree
.
├── app # railsアプリケーションディレクトリ
└── docker # dockerの関連ディレクトリ
├── bin # docker-compose用のディレクトリ
├── db # mysql用のdockerディレクトリ
└── web # rails用のdockerディレクトリ
以下作業のカレントディレクトリは上記の.
の位置で行います。
- mysql用のdockerファイル
docker/db/Dockerfile
今回はrails側のコンテナ構築がメインなのでとりあえずデータの永続化はしません。
FROM mysql:5.7
- rails用のdockerファイル
docker/web/Dockerfile
起動時にapp
フォルダをマウント出来るようにwebコンテナの/var/myapp
をVOLUME
にしておきます。
FROM ruby:2.3
############################################################
# ruby setting bundlerだけはglobalに入れておく
RUN gem install bundler
#####################################################
# misc settings
RUN usermod -u 1000 www-data
RUN \cp -fp /usr/share/zoneinfo/Asia/Tokyo /etc/localtime
VOLUME /var/myapp
WORKDIR /var/myapp
EXPOSE 3000
CMD ["bash"]
-
docker-compose
用ファイルdocker-compose.yml
は中で環境変数が展開出来るので、各Dockerディレクトリへのパスを環境変数で定義しておくためにdocker-compose
を実行するためのラッパースクリプトを作ってdocker-compose
を実行します。-
docker/bin/run-docker-compose.sh
docker/bin/run-docker-compose.sh#!/bin/sh SCRIPT_DIR=$(cd $(dirname $0) && pwd) export DB_DOCKERFILE_DIR=${SCRIPT_DIR}/../db export WEB_DOCKERFILE_DIR=${SCRIPT_DIR}/../web export WEB_APP_ROOT_DIR=${SCRIPT_DIR}/../../app export MYSQL_DATABASE=mydb export MYSQL_USER=dbuser export MYSQL_PASSWORD=dbuser_pass export MYSQL_ROOT_PASSWORD=root YML_FILE=$SCRIPT_DIR/docker-compose.yml docker-compose -f $YML_FILE $*
-
docker/bin/docker-compose.sh
web
からdb
を参照出来るようにlinks
の設定とホスト側のrails
ディレクトリをマウントするためのvolumes
設定をしておきます。docker/bin/docker-compose.shdb: container_name: db build: ${DB_DOCKERFILE_DIR} ports: - "3306:3306" environment: MYSQL_DATABASE: ${MYSQL_DATABASE} MYSQL_USER: ${MYSQL_USER} MYSQL_PASSWORD: ${MYSQL_PASSWORD} MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD} web: container_name: web build: ${WEB_DOCKERFILE_DIR} ports: - "3000:3000" links: - db:db volumes: - ${WEB_APP_ROOT_DIR}:/var/myapp environment: DATABASE_NAME: ${MYSQL_DATABASE} DATABASE_USER: ${MYSQL_USER} DATABASE_PASS: ${MYSQL_PASSWORD} DATABASE_HOST: db command: sh -c "bundle install --path=vendor/bundle && rm tmp/pids/server.pid; bin/rails s -b 0.0.0.0"
-
コンテナ作成
-
docker build
&rails
ディレクトリ作成これでコンテナが起動できるようになったので、
docker-compose build
してイメージ作成します。docker/bin/run-docker-compose.sh build
次にwebコンテナに入っている
ruby
でrails
のアプリケーションを初期化します。docker run --rm -it -v $(pwd)/app:/var/myapp bin_web bash #docker-composeでイメージを作成するとbin_${yamlの定義名}になります。 # 以降このコードブロックの終わりまでコンテナ内での作業 # Gemfile初期化 bundle init # この記事を作成中の最新の5.0.0.beta3を指定します。 echo "gem 'rails', '5.0.0.beta3'" >> Gemfile # プロジェクトローカルにrails5をインストール bundle install --path=vendor/bundle # railsのアプリケーションを作成します。 # とりあえず雛形のGemfileを作りたいだけなのであとでこのアプリケーションは全部削除します。 bundle exec rails new sample_app --skip-bundle #できたGemfileを持ってきます。 mv sample_app/Gemfile . #sample_appはもういらないので削除します。 rm -rf sample_app # 次にrailsのアプリケーションを作っていきますがファイルの編集はホストの方がやりやすいので一旦コンテナから出ます。 exit
-
Gemfile設定
ホスト側でGemfileを変更します。ここでは
sqlite
の替わりにmysql2
に変えたのとtherubyracer
のコメントを外しただけで後は全部rails new
が作成してくれたままです。source 'https://rubygems.org' # Bundle edge Rails instead: gem 'rails', github: 'rails/rails' gem 'rails', '>= 5.0.0.beta3', '< 5.1' # Use sqlite3 as the database for Active Record gem 'mysql2' # Use Puma as the app server gem 'puma' # Use SCSS for stylesheets gem 'sass-rails', '~> 5.0' # Use Uglifier as compressor for JavaScript assets gem 'uglifier', '>= 1.3.0' # Use CoffeeScript for .coffee assets and views gem 'coffee-rails', '~> 4.1.0' # See https://github.com/rails/execjs#readme for more supported runtimes gem 'therubyracer', platforms: :ruby # Use jquery as the JavaScript library gem 'jquery-rails' # Turbolinks makes navigating your web application faster. Read more: https://github.com/turbolinks/turbolinks gem 'turbolinks', '~> 5.x' # Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder gem 'jbuilder', '~> 2.0' # Use Redis adapter to run Action Cable in production # gem 'redis', '~> 3.0' # Use ActiveModel has_secure_password # gem 'bcrypt', '~> 3.1.7' # Use Capistrano for deployment # gem 'capistrano-rails', group: :development group :development, :test do # Call 'byebug' anywhere in the code to stop execution and get a debugger console gem 'byebug' end group :development do # Access an IRB console on exception pages or by using <%= console %> in views gem 'web-console', '~> 3.0' gem 'listen', '~> 3.0.5' # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring gem 'spring' gem 'spring-watcher-listen', '~> 2.0.0' end # Windows does not include zoneinfo files, so bundle the tzinfo-data gem gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]
-
再度コンテナで
bundle install
してrails
アプリケーション作成docker run --rm -it -v $(pwd)/app:/var/myapp bin_web bash # 以降このコードブロックの終わりまでコンテナ内での作業 # dbmsはmysqlにする。 bundle install && bundle exec rails new . -d mysql
このとき
Overwrite /var/myapp/Gemfile? (enter "h" for help) [Ynaqdh]
と、Gemfileを上書きするかと聞いてきますが、先ほど設定したのでn
にしました。 -
database設定
ホストに戻って
docker-compose
でlinkするmysql
に対しての接続設定をしておきます。
database.ymlはrails
がロードする前にerb
で処理するので任意のerb
の式が使えるため、そこでコンテナ起動時に渡している環境変数から接続情報を設定します。app/config/database.yml<% database_host = ENV['DATABASE_HOST'] || 'localhost' database_port= ENV['DATABASE_PORT'] || 3306 %> default: &default adapter: mysql2 encoding: utf8 pool: 5 username: <%= ENV['DATABASE_USER'] %> password: <%= ENV['DATABASE_PASS'] %> port: <%= database_port %> host: <%= database_host %> development: <<: *default database: <%= ENV['DATABASE_NAME'] %> # testとproductionは省略
-
コンテナの起動
これで設定ができたので、docker-compose
でup
します。(バックグラウンドにするならup -d
)docker/bin/run-docker-compose.sh up
db
とweb
のコンテナが正常に起動した後、docker-machine ip
で確認したipの3000番portにアクセスしてrailsのwelcomeページが表示されれば一旦設定完了です。
git 登録
ここまででgit
で管理出来るようになったのでgit init
したいですが、app/vendor
以下とapp/tmp
以下はgitに入れたくないので、.gitignore
の設定を追加します。
app/.gitignore
app/tmp/*
app/vendor/*
で、git
に登録します。
git init
git add -A
git commit -m "initial commit"
困った点
上記までで一旦railsの起動までいったんですが、ここからいざ開発しようとしたらいろいろ問題があって、主にdocker-machine
(というかvirtual box
などのshared folder
でソースをホストと共有するがゆえの)問題だったので結構ハマりました。
リロードされない問題
これで環境ができたので適当にコントローラを作ってみます。
docker exec -it web bash
でアタッチします。
# hello コントローラ作成
bundle exec bin/rails g controller hello
コントローラとビューを作成
app/controllers/hello_controller.rb
class HelloController < ApplicationController
def hello
@msg = "world"
end
end
app/views/hello.html.erb
Hello, <%= @msg %>
ルーティングも1つ追加します。app/config/routes.rb
Rails.application.routes.draw do
# For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
# Serve websocket cable requests in-process
# mount ActionCable.server => '/cable'
get 'hello' => 'hello#hello'
end
一応docker/bin/run-docker-compose.sh down; docker/bin/run-docker-compose.sh up
でクリーンな状態で起動します。
これでhttp://$(docker-machine's ip)/hello
にアクセスします。
きちんと表示されます。
次に、app/controller/hello_controller.rb
を編集してメッセージ(world
の部分)を変更してみます。
class HelloController < ApplicationController
def hello
@msg = "docker"
end
end
これでhello, world
からhello, docker
になるはずです、がreloadしてもhello, world
のままです。
で、いろいろ紆余曲折してたどりついたのが、
- ファイルの変更を検知しているのは
app/config/environments/development.rb
のfile_watcher
の部分。 - その設定ファイル内で
config.file_watcher = ActiveSupport::EventedFileUpdateChecker
によって検知している - ただ、その検知方法がファイルの変更イベントを受けて検知しているが
docker-machine
の共有ファイルに関してはその変更イベントが発生しない。
(これはdocker-machine
の問題でなく、共有ファイルとしてホスト側をマウントする方式では仕様としてどうしようもないそうです。)
ということで、なんとかファイルの変更をトリガーに変更を検知するのではなく、ポーリング形式でファイルの変更を監視する方法が必要なようでした。
で、いろいろ調べてみると、昔はその方式だったようでその方式でファイルの変更を検知すると自動でリロードされました。。
- app/config/environments/developments.rbで、既に定義されている
config.file_watcher
を下記のものに置き換えます。
config.file_watcher = ActiveSupport::FileUpdateChecker
変更後、docker/bin/run-docker-compose.sh down; docker/bin/run-docker-compose.sh up
でクリーンな状態で起動します。
で、app/controller/hello_controller.rb
を再度編集してみます。
class HelloController < ApplicationController
def hello
@msg = "docker docker docker"
end
end
でもやっぱり変更されません。
で、いろいろ調べてみると、docker-machine
でvirtual box
をドライバにして普通に作成すると、ホスト側と時刻が微妙にずれるとのこと。
そのずれた時間のせいでホストの時刻の方が未来時間になっていると上で設定したActiveSupport::FileUpdateChecker
がリロードしない、という処理になっていました。
# This method returns the maximum mtime of the files in +paths+, or +nil+
# if the array is empty.
#
# Files with a mtime in the future are ignored. Such abnormal situation
# can happen for example if the user changes the clock by hand. It is
# healthy to consider this edge case because with mtimes in the future
# reloading is not triggered.
def max_mtime(paths)
time_now = Time.now
paths.map {|path| File.mtime(path)}.reject {|mtime| time_now < mtime}.max
end
ここですね。最大の更新時刻を取得するんですが、「ファイルの更新時間が未来になっているようなファイルはabnormal
なので無視する」となっています。最初この処理を知らずに、「未来日付なんだから絶対更新されているとみなされるだろう」と思い込んでいたので相当ハマりました。やっぱりソースを読むのは大事ですね。
今回僕が試した環境では6分ぐらいホストとdocker-machine
で時刻がずれていました。その場合docker-machine
のドライバがvirtual box
の場合はdocker-machine ssh
でdocmer-machine
にログインした上で
sudo VBoxControl guestproperty set "/VirtualBox/GuestAdd/VBoxService/--timesync-set-threshold" 5000
で、docker-machine
のベースになっているvirtual box
の時刻とホストマシンの時刻のズレを抑える(ここでは5秒(=5000))ことで解決しました。
ココにたどり着くまでだいぶ時間ロスしました。。
web-console
のエラー問題
これで行けるかもと思いましたが、いざ表示するとweb-console
のエラー。。。
これは、docker-machine
でアクセスしているホスト(mac)と実際にアプリがあるホスト(docker-machine)が違うせいでした。
どこからでもweb-console
にアクセスできるようにして解決
app/config/environments/development.rb
config.web_console.whitelisted_ips = %w( 0.0.0.0/0 ::/0 )
まとめ
以上の考慮をいれたものをhttps://github.com/pocari/rails5-docker-sample/tree/ver01-rails-on-docker-setupにあげました。