search
LoginSignup
106

More than 5 years have passed since last update.

posted at

updated at

Organization

dockerでrails5環境構築

dockerでrails環境構築

rails5 + mysqlの開発環境が必要になったので、dockerコンテナとして構築します。
docker-machineで作業するにあたってものすごくハマったのでその作業記録も。

作業環境

railsmysqlはそれぞれ独立したコンテナにしたかったのでdocker-composeでまとめます。
またホストマシンはmacなのでdocker-machinedockerの環境を作っています。

  • 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側のコンテナ構築がメインなのでとりあえずデータの永続化はしません。

docker/db/Dockerfile
  FROM mysql:5.7
  • rails用のdockerファイル docker/web/Dockerfile

起動時にappフォルダをマウント出来るようにwebコンテナの/var/myappVOLUMEにしておきます。

docker/web/Dockerfile
  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.sh
      db:
        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コンテナに入っているrubyrailsのアプリケーションを初期化します。

     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-composeupします。(バックグラウンドにするならup -d)

     docker/bin/run-docker-compose.sh up
    

dbwebのコンテナが正常に起動した後、docker-machine ipで確認したipの3000番portにアクセスしてrailsのwelcomeページが表示されれば一旦設定完了です。

スクリーンショット 2016-04-20 0.04.10.png

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にアクセスします。
スクリーンショット 2016-04-20 0.34.53.png

きちんと表示されます。

次に、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.rbfile_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-machinevirtual boxをドライバにして普通に作成すると、ホスト側と時刻が微妙にずれるとのこと。
そのずれた時間のせいでホストの時刻の方が未来時間になっていると上で設定したActiveSupport::FileUpdateCheckerがリロードしない、という処理になっていました。

./bundle/ruby/2.3.0/gems/activesupport-5.0.0.beta3/lib/active_support/file_update_checker.rb
    # 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 sshdocmer-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にあげました。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
106