Railsチュートリアル 第3章

Railsチュートリアルの第3章を進めた際のメモです. チュートリアルに書かれていることは書かないようにしています. かなり個人的なメモですから, 参考程度にご覧ください.

3.1 セットアップ

Dockerを使って環境を構築しました. 詳しくはこちらで解説しています.

その他は省略します.

3.2 静的ページ

トピックブランチを作成しチェックアウト.

git checkout -b static-pages

3.2.1

railsコマンドはコンテナの中で実行しなくてはなりませんから, docker-compose execを使うとよいでしょう.

docker-compose exec -T web rails generate controller StaticPages home help

デフォルトでは標準入出力にTTYが割り当てられてしまいますが, オプション-Tでそれをオフにしています.

rails destroyも試したあと, git commitしておきました.

3.3 テストから始める

「テスト駆動」にするのか「一括テスト」にするのかについてのガイドラインは, 今後ためになりそうですね.

3.3.1

テストを実行します.

docker-compose exec -T web rails test

気なる文字列が表示されましたが, テストはうまくいったようです.

Running via Spring preloader in process 168
/myapp/db/schema.rb doesn't exist yet. Run `rails db:migrate` to create it, then try again. If you do not intend to use a database, you should instead alter /myapp/config/application.rb to limit the frameworks that will be loaded.
Run options: --seed 47859

# Running:

..

Finished in 1.537890s, 1.3005 runs/s, 1.3005 assertions/s.

2 runs, 2 assertions, 0 failures, 0 errors, 0 skips

3.3.2

指示通り実行し, テストはひとつだけ失敗しました. テストコードはDRY原則にあてはまっていないですが, これはきっと3.4.3でやるんでしょうね.

3.3.3

ルーティングの設定を書き加えて再びテストを実行します. テストは失敗しますが, エラーメッセージが変わりました.

アクションを追加してテストを実行します. テストは失敗しますが, またエラーメッセージが変わりました.

Viewファイルを追加することにより, やっとテストにパスしました. ブラウザでも確認ができました.

3.4 少しだけ動的なページ

レイアウトが自動設定されないように, レイアウトファイルの名前を変更しておきます. ただしGitにそれを知らせるため, git mvコマンドを使います.

git mv application.html.erb layout_file

3.4.1

テストを書き, 失敗することを確認.

3.4.2

タイトルを追加し, テストをパス.

演習1

テストコードには繰り返しの部分がたくさんあったので, インスタンス変数でまとめます.

3.4.3

レイアウトを利用することで, 各Viewファイルがかなりスッキリしました.

演習1

まずテストコードを書きます.

# test/controllers/static_pages_controller_test.rb

test "should get contact" do
  get static_pages_contact_url
  assert_response :success
    assert_select "title", "Contact | #{@base_title}"
end

テストを実行し, 正しいエラーメッセージが出ることを確認します.

$ docker-compose exec -T web rails test
NameError: undefined local variable or method `static_pages_contact_url'

URLがないというエラーがでているので,ルーティング設定を追加しましょう.

# config/routes.rb

Rails.application.routes.draw do
  get 'static_pages/home'
  get 'static_pages/help'
  get 'static_pages/about'
  get 'static_pages/contact' # 追加
end

再びテストを実行すると, エラーメッセージが変わります.

$ docker-compose exec -T web rails test
AbstractController::ActionNotFound: The action 'contact' could not be found for StaticPagesController

アクションがないというエラーが出ているので, アクションを追加しましょう.

# app/controllers/static_pages_controller.rb 

class StaticPagesController < ApplicationController
  #追加
  def contact
  end
end

再びテストを実行すると, さらにエラーメッセージが変わります.

$ docker-compose exec -T web rails test
ActionController::UnknownFormat: StaticPagesController#contact is missing a template for this request format and variant.

Viweファイルがないというエラーが出ているので作りましょう.

# app/views/static_pages/contact.html.erb

<% provide(:title, "Contact") %>
<h1>Contact</h1>
<p>
  Contact the Ruby on Rails Tutorial about the sample app at the
  <a href="https://railstutorial.jp/contact">contact page</a>.
</p>

テストもパスし, ブラウザでも確認することができます.

3.4.4

rootルーティングの設定をしました.

演習1

テストコードを追加し, テストを実行します. パスできました.

次にrootルーティングの設定をコメントアウトしてテストを実行します. すると以下のようなエラーメッセージが表示されました.

NameError: undefined local variable or method `root_url'

コメントアウトを外すとテストをパスできます.

3.5 最後に

現在のトピックブランチであるstatic-pagesmasterブランチにマージします.

3.6 高度なセットアップ

3.6.1

色付きで進捗状況が表示されるようになりました.

3.6.2

テストの自動化はDocker環境でも問題ないのでしょうか...? ひとまず試してみます.

docker-compose exec -T web bundle exec guard init

Guardfileが作成されるので, 指示通りに書き換えます. Gitspring filesを追跡しないように.gitignoreを編集します.

以下のコマンドを実行し, Guardをオン?にします.

$ docker-compose exec -T web bundle exec guard
05:38:32 - INFO - Guard::Minitest 2.4.6 is running, with Minitest::Unit 5.11.3!
05:38:34 - INFO - Guard is now watching at '/myapp'
[1] guard(main)> 

するとプロンプトが表示されたので, そのままEnterを押しました. するとテストが実行されて, 再び同じプロンプトが表示され待機状態になりました. exitを入力することで抜けられました.

[1] guard(main)> exit
exit

05:39:05 - INFO - Bye bye...

これでいいんでしょうか...?

05:39:05などの数字は時刻っぽいですね. でも現時刻は14:41. つまり世界標準時が表示されるようになっているようです.

以上で3章終えましたああ〜〜.

Railsチュートリアル 第2章

Railsチュートリアルの第2章を進めました. 概観をつかむ章ですね.

2.1 アプリケーションの計画

省略

2.2 Usersリソース

書かれている通りにコマンドを実行します. ただしコンテナの中に入る必要があるので, 以下を実行します.

docker exec web bash

するとコンテナに入ることができますから, チュートリアル通りに進めることができます.

$ rails generate scaffold User name:string email:string
$ rails db:migrate

ブラウザでlocalhost/usersへアクセスすると, チュートリアル通りになっています. rails serverは実行しなくてOKです.


2.2.1

好きなダーツプレイヤーの名前を入れていろいろ試しました.

演習1

ブラウザはSafariを使っているので,

Cmd + Option + U

でソースを表示します.

新しくユーザーを作成すると, 演習で問われている箇所が発見できます.

<p id="notice">User was successfully created.</p>

リロードすると, タグだけが残りました.

<p id="notice"></p>

演習2, 3

問題なくユーザーが作成できました. バリデーションはまだ実装されていないということですね.

演習4

Are you sure?というダイアログが表示されました.


2.2.2

MVCやRESTなどのキーワードが登場.

演習1

省略

演習2

app/controllers/users_controller.rbeditアクションを見てみると,

def edit
end

あれ? 空っぽ...

と思ったらprivate methodが定義されていました.

  private
    # Use callbacks to share common setup or constraints between actions.
    def set_user
      @user = User.find(params[:id])
    end

これが各アクションの前に実行されることは, UsersControllerのはじめの方を見るとわかります.

class UsersController < ApplicationController
  before_action :set_user, only: [:show, :edit, :update, :destroy]

演習3

以下の通りです.

app/views/users/edit.html.erb

2.3 Micropostsリソース

2.3.1

Usersリソースと同様ですね

演習1

これもUsersリソースと同様でした.

演習2

ContentもUserも空っぽであっても, Micropostは作成できました.

演習3

abc...xyzを6回入力して試してみましたが, 問題なく投稿できました.

演習4

Usersリソースの場合と同様にAre you sure?とダイアログが表示されました.


2.3.2

はじめmaximamとスペルミスをしてしまいましたが, バリデーションが追加されたことがチェックできました. ただエラーメッセージが真っ赤でどぎつい!

演習1

バリデーションを追加すると同時にチェックしてしまいました.

演習2

見てみましたが, これだけでよいのでしょうかね.

<div id="error_explanation">
  <h2>1 error prohibited this micropost from being saved:</h2>

  <ul>
    <li>Content is too long (maximum is 140 characters)</li>
  </ul>
</div>

2.3.3

指定された通りにリレーションを定義します.

rails consoleを使うためにコンテナの中に入ります.

$ docker-compose exec web bash

対話的に, ひとつのuserに複数のmicropostsが結びついていることがわかりました.

演習1

userの取得はすでにコントローラーに実装されているので, Viewファイルだけを編集すればいいですね. 以下をshow.html.erbに追加すればよいです.

# app/view/users/show.html.erbの一部

<p>
  <strong>First micropost:</strong>
  <%= @user.microposts.first.content %>
</p>

演習2, 3

指示通りに編集し, バリデーションが設定されたことが確認できました.

2.3.4 以降

演習もありますが簡単なので省略します.

Railsチュートリアル 第1章

環境構築

開発環境はDockerを使って構築しました. 詳細な手順は以前の投稿にて詳しく解説しています.

環境構築の時点で, rails newまで終わらせています. ファイルを編集する前にGitリポジトリを作成しました.

Gemfileの編集

チュートリアル通りのバージョンではなく, rails newによってインストールされた番号を指定するようにしました.

jquery-railsはインストールされなかったようなので, 最新バージョンを指定してGemfileに追記しました.

sqlite3もインストールされていませんでしたが, DBコンテナにMySQLをインストールしているので, 必要ないと判断しました.

またWindowsに必要であると書かれているGemの指定はコメントアウトしました.

公式ドキュメントに書かれているように,

Now that you’ve got a new Gemfile, you need to build the image again. (This, and changes to the Gemfile or the Dockerfile, should be the only times you’ll need to rebuild.)

Gemfileに変更があった場合はイメージを再構築します.

docker-compose build

Dockerファイルにbundle installと書いているので, jquery-railsがしっかりインストールされます.

Gemfileの変更を記録するためにgit commitしておきました.

その他

リモートリポジトリ, herokuのあたりはまた今後やっていこうと思います.

DockerでRails+MySQLの環境を構築

Railsチュートリアルを進めるための環境を構築しました.

概要

基本的にはDockerの公式ドキュメントに則って行いました. Rails用のコンテナとDBサーバー用のコンテナをそれぞれDockerfileから用意し, それらを連携するためにdocker-composeを用いています. データベースとしてはMySQL, WebサーバーとしてはApache等ではなくRails serverを使います.

なぜそう記述するのか, なぜそのコマンドなのか, 等の理由もできるだけ述べていきます. ではやっていきましょう.

作業環境

マシンOSとDockerのバージョンです.

$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.13.5
BuildVersion:   17F77

$ docker version
Client:
 Version:      18.03.1-ce
 API version:  1.37
 Go version:   go1.9.5
 Git commit:   9ee9f40
 Built:        Thu Apr 26 07:13:02 2018
 OS/Arch:      darwin/amd64
 Experimental: false
 Orchestrator: swarm

Server:
 Engine:
  Version:      18.03.1-ce
  API version:  1.37 (minimum version 1.12)
  Go version:   go1.9.5
  Git commit:   9ee9f40
  Built:        Thu Apr 26 07:22:38 2018
  OS/Arch:      linux/amd64
  Experimental: true

作業ディレクトリは以下のような構成です.

myapp/
├── docker-compose.yml
├── db/
│   ├── Dockerfile
│   ├── conf/
│   └── data/
└── web/
    ├── Dockerfile
    ├── Gemfile
    └── Gemfile.lock

myapp/web/にはrailsのファイルが入り,
myapp/web/conf/にはMySQLの設定ファイル, myapp/db/data/にはMySQLのデータが入ります.

ひとつの疑問

ドットインストール等を見てみると, ローカル開発環境の構築にはCentOSが多く使われているように感じます. しかしDockerHubにある公式イメージを見てみると, Debianがベースとなっているものが多い印象です. なぜDockerではDebianが多く使われているのでしょうか. 理由は公式ドキュメント(日本語版)にありました.

私たちは Debian イメージ を推奨します。これは、非常にしっかりと管理されており、ディストリビューションの中でも小さくなるよう(現在は 150 MB 以下に)維持されているからです。

日本語訳されたドキュメントは少し情報が古いので, 英語の公式ドキュメントも見てみましょう.

We recommend the Alpine image as it is tightly controlled and small in size (currently under 5 MB), while still being a full Linux distribution.

こっちではAlpineを推奨していますね. しかしこのブログ記事を書くまで英語のほうは見ていませんでしたので, 本記事ではDebianをベースとして構築しています.

各種ファイル

5つのファイルを自分で用意します.

MySQLコンテナ用のDockerfile

# myapp/db/Dockerfile

# ベースとなるイメージを指定
FROM mysql:5.7

# MySQLのルートユーザー用のパスワードを設定
ENV MYSQL_ROOT_PASSWORD=password

データベースのほうはこれだけです. 同じ内容をComposeファイルに書くこともできるので, 必ずしもこのDockerfileは必要ではありません. しかし今回は練習だと思って作りました. もしも好み通りにカスタマイズしたい場合はDockerfileに書くことになるでしょう.

ベースイメージとしてはmysql:5.7を指定しました. mysql:8ではうまくいきませんでした.

上でも触れたようにこのイメージはDebianがベースになっています. したがって当然ながらFROM debianのようにOSのイメージを指定することもできます. しかしそうすると, DockerfileにMySQLをインストールする命令等も書かなくてはならないので非常に面倒です. 実際mysql:5.7のもとになっているDockerfileはかなりの行数が書かれていますしね. 素直に出来上がったものを採用するのが賢明でしょう.

ENV命令ではコンテナ内の環境変数を設定することができます. セキュリティ上はこんなところにパスワードを書くのはよろしくないんでしょうけど, Railsの練習用なのでよしとしましょう.

Webサーバーコンテナ用のDockerfile

# myapp/web/Dockerfile

# Debianがベースのrubyイメージを指定
FROM ruby:2.5.1

# 必要なものをインストール
RUN apt-get update -qq && apt-get -y install \
    build-essential \
    libpq-dev \
    nodejs \
    mysql-client

# rails用のディレクトリを作成
RUN mkdir /myapp

# ローカルマシン(Mac)からコンテナの中にファイルをコピー
COPY Gemfile /myapp
COPY Gemfile.lock /myapp

# 作業ディレクトリを指定
WORKDIR /myapp

# 上でコピーしたGemfileに従ってGemをインストール
RUN gem install bundler && bundle install

こちらは公式ドキュメントを真似して書いています. しかし以下の5点異なるところがあります.

  • ひとつめのRUN命令では, WebサーバーのコンテナからMySQLにアクセスするために, クライアントコマンドをインストールしています.
  • COPY命令の第二引数ではディレクトリだけを指定しています. ファイル名を変えるわけではないのでこれでよいでしょう.
  • WORKDIR命令の位置が異なります. とくにCOPY命令で使っているわけではないのでこれでOKです.
  • bundlerをインストールしています. はじめからbundlerが導入されているのか不明だったのでこうしておきました.
  • 最後のCOPY命令がありません. 結局Composeファイル内でボリュームのマウントを指定するので必要ないと思われます.

2つのコンテナをまとめて扱うComposeファイル

# myapp/docker-compose.yml

version: '3'
services:
  web:
    build: ./web
    ports:
      - "80:3000"
    depends_on:
      - db
    volumes:
      - ./web:/myapp
    command: bundle exec rails s -p 3000 -b '0.0.0.0'
  db:
    build: ./db
    volumes:
      - ./db/data:/var/lib/mysql
      - ./db/conf:/etc/mysql/conf.d
    expose:
      - "3306"

初めてYAMLを書きました. タブ文字を使ってはいけないというところは注意が必要ですね. コメントで説明すると見づらくなりそうなので, 以下で詳細を述べていきます.

  • version:ではComposeファイルのフォーマットのバージョンを指定しています. 2018年6月現在では3.6が最新ですが, 小数まで指定する必要はなさそうです.
  • services:によってwebとdbの2つのサービスを定義しています.
  • build:によって, 各サービスに対応したDockerfileの場所を指定しています.
  • ports:はポートフォワーディングの指定です. 時刻と認識されないようにダブルクォートが必要です. localhost:3000と指定するのは面倒なのでwell-knownポートの80を指定しています.
  • depends_on:により2つのサービスの依存関係を定義しています. これにより, dbが立ち上がった後にwebが立ち上がるようにしてくれます.
  • volumes:により, ローカルマシンのボリュームをマウントすることができます. これがあるので, Dockerfile内にCOPY . /myapp命令は必要ないでしょう. ボリュームのマウントについては詳しくこちらにまとめておきました.
  • command:はコンテナ起動時にコンテナ内で実行するコマンドです. DockerfileにWORKDIR /myappと書いたおかげで, /myappでこのコマンドを実行してくれます. rails sというコマンドはRails serverを起動するものです. デフォルトの3000ポートを指定していますね.
  • expose:により, 他のサービス(この場合はweb)へポートを解放します.

GemfileとGemfile.lock

Gemの一種であるRailsをインストールするので, Gemfileを作成し, コンテナ内にコピーします. Gemfile.lockは内容が空っぽのファイルを作成するだけでOKです.

source 'https://rubygems.org'
gem 'rails', '5.2.0'

Railsアプリの骨組みを構築

Composeファイルがあるディレクトリであることを確認して, 以下のコマンドを実行します.

$ pwd
/myapp
$ docker-compose run web rails new . --force --database=mysql

このコマンドはdocker run <image>と同じような働きをします. しかしdocker-composeの場合はサービス名 (この例ではweb)で指定できるのが便利ですね.

rails newがコンテナ内で実行されます. アプリ名を引数に指定してこのコマンドを実行することにより, 指定した名前でアプリの骨組みが作成されます. この場合はカレントディレクトリを表す.が引数となっているので, カレントディレクトリ名がアプリ名に指定されたことになります. DockerfileにおいてWORKDIR /myappと記述しているので, rails newが実行されるディレクトリは/myappです. したがってmyappという名前のアプリが作られることになるわけですね. またRailsによって自動生成された各種ファイルはこのディレクトリに配置されることになります.

--forceはファイルの上書きを許可するオプションです. rails newは新たにGemfileを作成しますが, このオプションにより先ほど用意したGemfileが上書きされます.

--database=mysqlは見たまんまですね. データベースの種別を指定しています.

Composeファイルにてcommand:を指定していましたよね. これはコンテナが起動するときに発動すると言いました. しかし今回のdocker-compose run実行時には発動しません. それはrails newで上書きされたからです. command:で指定したコマンドは上書きが可能なのです. はじめだけrails newを上書きで実行しておいて, 一旦環境が構築されてしまえば, もとのコマンドによりRails serverが起動してくれるということです. うまいことできてますね.

イメージを再構築

rails newしたことにより, Gemfileが上書きされました. この状態を保持するためにイメージを再構築します. そのコマンドがdocker-compose buildです. 公式ドキュメントには以下のように書かれています.

Now that you’ve got a new Gemfile, you need to build the image again. (This, and changes to the Gemfile or the Dockerfile, should be the only times you’ll need to rebuild.)

つまりGemfileやDockerfileに変更があった場合はこのコマンドでrebuild(再構築)しろってことですね.

データベース設定をRailsに教える

データベースの設定ファイルを編集します. ボリュームをマウントしているので, コンテナの中に入らずとも編集ができますね. ファイルパスはmyapp/web/config/database.ymlです.

# MySQL. Versions 5.1.10 and up are supported.
#
# Install the MySQL driver
#   gem install mysql2
#
# Ensure the MySQL gem is defined in your Gemfile
#   gem 'mysql2'
#
# And be sure to use new-style password hashing:
#   https://dev.mysql.com/doc/refman/5.7/en/password-hashing.html
#
default: &default
  adapter: mysql2
  encoding: utf8
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  username: root
  password: password    # 変更箇所!!
  host: db    # 変更箇所!!

development:
  <<: *default
  database: myapp_development    # 変更可能!!

# Warning: The database defined as "test" will be erased and
# re-generated from your development database when you run "rake".
# Do not set this db to the same as development or production.
test:
  <<: *default
  database: myapp_test    # 変更可能!!

# As with config/secrets.yml, you never want to store sensitive information,
# like your database password, in your source code. If your source code is
# ever seen by anyone, they now have access to your database.
#
# Instead, provide the password as a unix environment variable when you boot
# the app. Read http://guides.rubyonrails.org/configuring.html#configuring-a-database
# for a full rundown on how to provide these environment variables in a
# production deployment.
#
# On Heroku and other platform providers, you may have a full connection URL
# available as an environment variable. For example:
#
#   DATABASE_URL="mysql2://myuser:mypass@localhost/somedatabase"
#
# You can use this database configuration with:
#
#   production:
#     url: <%= ENV['DATABASE_URL'] %>

はじめに設定したrootユーザーのパスワードを書きましょう. またホストにdbを指定します. Dockerのおかげでサービス名で指定することができます.

開発環境とテスト環境のデータベース名があらじめ指定されていますが, 自分の使いやすい名前に変更することもできます.

サービスの立ち上げ

データベース設定ができたので, いよいよサービスの立ち上げです.

$ docker-compose up -d

-dオプションによりバックグラウンドでサービスを起動することができます.

ではさっそくブラウザで localhostにアクセスしてみましょう. するときっとエラーが出ると思います. これはデータベースが作成されていないことが原因です. ではデータベースを作成しましょう. 以下のコマンドを実行します.

$ docker-compose exec web rails db:create

公式ドキュメントではrake db:createを実行していますが, これは古いタイプのコマンドです. Rails5を使う場合はrails db:createを使いましょう.

さてこれでデータベースも作成されました. 再びlocalhostへアクセスしてみましょう. どうでしょうか. Yay! You're on Rails!が表示されれば成功です.

今後はこの環境を使ってRailsチュートリアルを進めていきます.

Dockerにおけるボリュームのマウント

Dockerを使う際, ホストマシン上のボリュームをコンテナ内にマウントしたい場面が多々あります. 本記事ではDockerにおけるマウントについて深く掘り下げていきます. 本記事にはMacだけにしか適用できない事柄を含みます(私がMacを使っているので). その場合はそうだとわかるように断りを入れることとします.

マウントとは

ボリュームのマウントとは, 平たく言えばホストマシンとコンテナでファイルを同期することですね. たとえばホストマシンの~/workとコンテナ内の/myappを同期させ, 片方でファイルを編集すれば, もう片方でもその変更が確認できる状態になります.

しかし同期させる前のディレクトリ内の状態が異なっている場合, どのように同期した状態までもっていくのでしょうか. たとえばそれぞれのディレクトリ内の状態が以下のようになっていたとしましょう.

~/work
    └── file1.txt

/myapp
    └── file2.txt

この状態から2つのディレクトリを同期させるとしたら, 次のようになることが期待されるでしょう.

~/work
    ├── file1.txt
    └── file2.txt

/myapp
    ├── file1.txt
    └── file2.txt

つまり2つのディレクトリの和集合を考えたということです. しかし次のような場合はどうでしょう.

# ローカルマシン内で
~/work
    └── file1.txt

$ cat ~/work/file1.txt
Hello World.


# コンテナ内にて
/myapp
    └── file1.txt

$ cat /myapp/file1.txt
It's a beautiful day.

ファイル名が同じで内容が異なる場合, 和集合を取る操作ができません. では実際はどのような挙動をするのかというと, 片方がもう片方を上書きします. どちらが上書きする側で, どちらが上書きされる側なのかは, マウントの指定によります. つまりマウントには向きがあるということです. 実はDockerにおいては, ホストマシンがコンテナを上書きする, という方向しかありません(私が知っている例外としてMySQLがありますが, これについては本記事の最後に述べます).

ホストマシンとは

上で, ホストマシンとコンテナでディレクトリを同期させることができると述べました. 注意しておきたいのが, ここでいうホストマシンとはMacのことではありません. Mac上で起動している仮想Linuxマシンなのです. そもそもコンテナというものはLinuxカーネルに使われている技術です. したがってMacでコンテナ技術を使うためには, 間に仮想Linuxマシンをはさまなくてはならないというわけです.

ではMacとコンテナはデータのやりとりができないのかというとそうではありません. 仮想Linuxマシンからは, Mac全体が/Macとして参照できます. つまりMac全体が仮想Linuxマシンにマウントされているわけですね. 他にもMac上の/Usersが仮想Linuxマシンからも/Usersでアクセスできます.

間に仮想Linuxマシンがあることは覚えておいたほうがよいとは思いますが, 以下の考察ではこれを意識することはあまりないと思います.

Dockerにおけるマウントの指定

Dockerによる環境構築をする際, ボリュームをマウントする方法はDockerfile, Composeファイル, コマンドラインの3つがあります.

1. Dockerfile

書式

ひとつはDockerfile内でVOLUME命令を書く方法です. この命令では引数としてコンテナ側のディレクトリしか指定することはできません.

VOLUME /myapp

挙動確認

具体的にどのような挙動なのかを確かめてみましょう. まずはDockerfileを用意します.

FROM debian
RUN mkdir /myapp && \
         echo "Hello, World" > /myapp/file.txt
VOLUME /myapp

挙動確認のためのコンテナですから, これだけで十分です. このDockerfileをもとにhelloという名前のイメージを作成し,

$ docker build -t hello .

コンテナを起動すると同時にコンテナ内に入ります.

$ docker run -it hello bash

するとちゃんとDockerfile内で作成したファイルが確認できます(プロンプトは$で表現しています).

$ cat /myapp/file.txt
Hello, World.

さて疑問なのは, /myappと同期されているディレクトリはどこにあるのか, ということですね. ホストマシン上のディレクトリを指定していませんから, Dockerが勝手に(自動で?)決めています. これは以下のコマンドを実行するとわかります(コンテナIDは最初の数桁だけでOKです).

$ docker inspect <container ID>

真ん中より下あたりにあるMountsの項目を見てみます.

        "Mounts": [
            {
                "Type": "volume",
                "Name": "e8e~略~ac2",
                "Source": "/var/lib/docker/volumes/e8e~略~ac2/_data",
                "Destination": "/myapp",
                "Driver": "local",
                "Mode": "",
                "RW": true,
                "Propagation": ""
            }
        ],

Sourceの値が, マウントされているホストマシン上のディレクトリです. NameSourceに見られる長い文字列はコンテナIDではなくボリュームIDですね. さてホストマシン上のディレクトリがわかったので, 中身を覗いてみましょう. ただしホストマシンは仮想Linexマシンであることを忘れないように. 仮想Linuxマシンの実体はMacから見ると以下のパス上に配置されています.

~/Library/Containers/com.docker.docker/Data/com.docker.driver.amd64-linux/Docker.qcow2

以下のコマンドを実行することにより, この仮想マシンの中に入ることができます.

$ screen Containers/com.docker.docker/Data/com.docker.driver.amd64-linux/tty

そこで,

cat /var/lib/docker/volumes/e82~略~ac2/_data/file.txt

とすれば, たしかにファイルが同期されていることが確認できます. 仮想マシンから抜け出すには, +Aを押したあとにKを押します. 確認ダイアログがでてくるので, Yを押してください. これで抜けられます.

なぜホストマシンのディレクトリを指定できないのか.

Dockerfileにボリュームのマウントを設定する際は, ホストマシンのディレクトリを指定することができません. 理由が公式ドキュメントに書かれています.

The host directory is declared at container run-time: The host directory (the mountpoint) is, by its nature, host-dependent. This is to preserve image portability, since a given host directory can’t be guaranteed to be available on all hosts. For this reason, you can’t mount a host directory from within the Dockerfile. The VOLUME instruction does not support specifying a host-dir parameter. You must specify the mountpoint when you create or run the container.

Dockerを使って環境構築をする理由は, どこでも誰でも同じ環境が手に入れられるからですね. それなのにDockerfileにホストマシン上のディレクトリを指定してしまったらどうでしょう. そのDockerイメージのユーザーは同じディレクトリ構成を持っていなければならなくなります. それよりは, イメージの各ユーザーがコンテナを起動する際に, ホストマシン上のディレクトリを指定するほうがいいだろうという考え方です. 理にかなっていますね.

2. Composeファイル

書式

Composeファイルではホストマシンのディレクトリも指定することができます.

volumes:
    - ./work:/myapp

各パスは以下のルールに則ります.

挙動確認

挙動をいくつかのパターンで試してみましょう. まずは以下のような構成を用意します.

docker_test/
├── docker-compose.yml
└── test/
    └── file.txt
$ cat test/file.txt
Hello, World.
例1

挙動確認に必要最低限なComposeファイルを用意します.

version: '3'
services:
  web:
    image: debian
    volumes:
      - ./test:/myapp

以下のコマンドにより, コンテナを起動し, コンテナの中に入りましょう.

$ docker-compose run web bash

webはComposeファイル内で定義したサービス名, bashはコンテナの中で実行するコマンドです. コンテナの中に, マウントされたファイルがあることが確認できます.

$ cat /myapp/file.txt
Hello, World.

/myappというディレクトリも自動的に作られていることもわかりますね.

例2

次はホストマシンのディレクトリの書き方を少し変えてみましょう.

version: '3'
services:
  web:
    image: debian
    volumes:
      - test:/myapp

さっきと同じコマンドでコンテナの中に入ろうとすると, エラーが出ます.

$ docker-compose run web bash
ERROR: Named volume "test:/myapp:rw" is used in service "web" but no declaration was found in the volumes section.

test:/myapp:rwという名前のボリュームがwebの中で使われているが, そんな名前をしたボリュームは見つからない」という意味ですね. サブディレクトリを指定する場合は./testのような書き方にしましょう.

例3

次はDockerファイルをComposeファイルと同じディレクトリに作成します.

FROM debian
RUN mkdir /myapp && \
    echo "It's a beautiful day." > /myapp/anothe_file.txt

コンテナ内でanother_file.txtというファイルを作成しています. 現在のディレクトリ構成を確認しておきましょう.

docker_test/
├── Dockerfile
├── docker-compose.yml
└── test/
    └── file.txt

Dockerファイルを作成したので, Composeファイルも変更しなくてはなりません.

version: '3'
services:
  web:
    build: .
    volumes:
      - ./test:/myapp

この状態でコンテナを起動します. $ docker-compose run web bash するとコンテナ内に入ることができるので, Dockerファイルにて作成したファイルがあるかを確認してみます.

$ ls myapp
file.txt

anothe_file.txtは存在せず, file.txtだけが存在しています. はじめに述べたように, マウントには向きがあるからです. ホストマシンのディレクトリが上書きしてしまいました.

3. コマンドライン

コマンドラインからも

$ docker run -v ./test:/myapp -it <image> bash

のようにボリュームを指定することができます. また今後詳しく書きたいと思います.

MySQLにおけるマウント

MySQLはデータを/var/lib/mysqlというディレクトリに保存します. このディレクトリをローカルマシンにマウントすることで, データを「永続化」することができます. つまりコンテナを削除してもデータはローカルマシンに残せるようになります.

たとえばdocker-compose upで起動させたプロジェクトは, docker-compose downで停止することができますが, この際コンテナも一緒に削除されるようになっています(その理由などはこちらで詳しく述べています). しかしボリュームをマウントしておけば, テーブルや, テーブルに挿入したレコードなどを消えずに残ったままにしておけます.

これまで, Dockerにおけるマウントの向きは一方向に限られるということを述べてきました. しかしMySQLの公式イメージにおいては,

# docker-compose.ymlの一部
service:
  db:
    volumes: ./db/data:/var/lib/mysql

のように指定したとしても, /var/lib/mysql./db/dataにより上書きされることはありません. 実際コンテナ起動後に./db/dataを見てみると, MySQLのデータが確認できます.

MySQLの公式イメージを使う場合は, データの上書きを心配する必要はないということです.

docker-compose down はなぜコンテナを削除するのか

DockerでRailsの開発環境を構築しました. 公式ドキュメントを見てみると, プロジェクトの停止にはdocker-compose downを使えと書いてあります. このコマンドを実行するとコンテナが削除されますが, どうしてコンテナを削除する必要があるのでしょうか. 実験を通して考察していきます.

実験

コンテナをわざわざ消去するということは, 以下の2つに明確な差があるということです.

  • コンテナを作り直して起動する.
  • 停止したコンテナを再起動する.

ここで思い出すのが, DockerfileにおけるCMD命令ですね. これはコンテナを起動する際に発動させるコマンドを指定するものです. ではこれにフォーカスして実験してみましょう.

Dockerfileを作成します.

FROM debian
CMD echo "Hello, World."

イメージをビルドします. イメージ名はhelloにしましょう.

$ docker build -t hello .

コンテナを起動します.

$ docker run hello
Hello, World.

Dockerfile内で指定した通りのコマンドが実行されましたね. 現在のコンテナの状況を見てみましょう.

$ docker ps -a
CONTAINER ID   IMAGE   COMMAND                   CREATED       STATUS
bce34cc95f4e   hello   "/bin/sh -c 'echo \"H…"   8 second ago  Exited (0) 26 seconds ago

STATUSExitedになっています. つまりコンテナが停止していますね. echoというプロセスが終了したために, コンテナもそれに合わせて停止してしまったのです.

この状態からコンテナを再起動してみましょう.

$ docker start bce34
bce34

コンテナIDを返しただけで, echoは実行されませんでした.

考察

上の実験からわかることは, CMD命令で指定されたコマンドは, コンテナを構築し, 起動したときに発動するということです. Dockerfile内のCMD命令について, 公式ドキュメントにはこう書かれていました.

the CMD instruction sets the command to be executed when running the image

日本語訳しましょう.

CMD命令は, イメージを起動する際に実行されるコマンドを設定する

コマンドが実行されるのは, イメージの起動時だったわけです.

結論

コンテナを起動させるたびに, 指定したコマンドも同時に実行したいから, 毎回コンテナを削除していたのでした. Railsの環境を構築した際は, イメージ起動時のコマンドとしてRails serverの起動が指定されていました. コンテナを削除してくれていたおかげで, Rails serverも自動的に起動するようになっていたわけです.