こんにちは、鈴木です。
前回、前々回は Docker の基本を解説しました。
今回は Rails の開発環境を Docker 上に作り上げてみようと思います。
全体的な構成
全体的な構成は以下の通りです。
- アプリケーション名は hello とする。
- データベースには PostgreSQL を使用する。
- データベース用のコンテナと開発用のコンテナを作成する。
- 開発用のコンテナには SSH で接続する。
データベース用のコンテナを作成する
まずはデータベース用のコンテナを作成します。
Docker Hub で PostgreSQL のイメージが提供されているので、それを使います。
1 2 3 4 5 |
# イメージを取得する. docker pull postgres:9.3.5 # コンテナを起動する. docker run -d --name postgres -e LC_ALL=C.UTF-8 postgres:9.3.5 |
イメージを取得して起動するだけです。
コンテナはデタッチモードで動かしたいので -d オプションを指定しています。また、コンテナには名前を付けておくと便利なので --name で「postgres」という名前を付けました。
ポイントは環境変数として「-e LC_ALL=C.UTF-8」を指定しているところです。これは initdb でデータベースクラスタを初期化するときのエンコーディングを UTF-8 とするためです(デフォルトでは SQL_ASCII になります)。
Rails 開発用のイメージを作成する
次に Rails 開発用のイメージを作成します。
PostgreSQL は既存のイメージをそのまま使用しましたが、今度は個人用の開発環境を作りたいのでそうもいきません。
Dockerfile を書いて自分用のイメージを作成することにしました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 |
FROM ubuntu:14.04 MAINTAINER TECHSCORE # (1) パッケージのインデックスを更新する. RUN apt-get update # (2) 一般的なパッケージをインストールする. RUN apt-get install -y aptitude RUN apt-get install -y git RUN apt-get install -y subversion RUN apt-get install -y tree # ... 他にインストールしておきたいパッケージがあれば追加する. # (3) SSH サーバをインストールする. RUN apt-get install -y openssh-server RUN mkdir /var/run/sshd RUN ssh-keygen -A RUN sed -ie 's/^\(session \+required \+pam_loginuid.so\)/#\1/' /etc/pam.d/sshd # (4) RVM に必要なパッケージをインストールする. RUN apt-get install -y curl # (5) Rails に必要なパッケージをインストールする. RUN apt-get install -y postgresql-client-9.3 RUN apt-get install -y postgresql-server-dev-9.3 RUN apt-get install -y ruby-execjs # (6) 開発用のユーザを作成する. RUN useradd -m -s /bin/bash rails RUN echo 'rails:password' | chpasswd RUN echo 'rails ALL=(ALL) NOPASSWD:ALL' > /etc/sudoers.d/rails # (7) rails ユーザに切り替える. USER rails ENV HOME /home/rails # (8) RVM をインストールする. RUN curl -sSL https://get.rvm.io | bash -s stable RUN bash -l -c 'rvm requirements' # (9) Ruby をインストールする. RUN bash -l -c 'rvm get stable' RUN bash -l -c 'rvm install ruby-2.1.2' RUN bash -l -c 'rvm use --default ruby-2.1.2' # (10) dotfiles (.gemrc). RUN echo 'gem: --no-ri --no-rdoc' > ~/.gemrc # (11) Rails をインストールする. RUN bash -l -c 'gem install rails -v 4.1.4' # (12) 作業用のディレクトリを作成する (この中に Rails アプリケーションを作成する). RUN mkdir ~/workspace # (13) root ユーザに切り替える. USER root # (14) コンテナ外に出すポートを指定する. EXPOSE 22 3000 # (15) 外部からマウントする場所を指定する. VOLUME /home/rails/workspace # (16) 起動時のコマンドを指定する. CMD ["/usr/sbin/sshd", "-D"] |
まとまりごとにコメントを記載しましたので、順番に見ていきましょう。
(1) の apt-get update ではパッケージのインデックスを更新しています。このような一般的な処理は Dockerfile の先頭のほうに記述しておくと良いでしょう。そうすることで、試行錯誤しながら Dockerfile を修正&ビルドを繰り返すときの作業時間を短くすることができます。というもの、Dockerfile をビルドする時は 1 行 1 行が順番に実行され、中間イメージとしてコミットされます。中間イメージはキャッシュされるため、Dockerfile の最後のほうを修正してビルドしなおした場合、既にあるキャッシュが流用され、変更した部分移行が新たにビルドされるためです。反対に Dockerfile の先頭の方を変更すると、ほとんどキャッシュを流用することができないため、ビルド時間はあまり短縮できません。
(2) では一般的なパッケージをインストールしています。他に必要なパッケージがあれば、ここに追加しましょう。
(3) では SSH サーバをインストールし、必要な設定を行っています。
(4), (5) はそれぞれ RVM (Ruby Version Manager) と Ruby のインストールに必要なパッケージをインストールしています。
(6) では開発用のユーザとして rails という名前のユーザを作成しています。パスワードは「password」としているので、適宜変更してください。また、パスワード無しで sudo できるように設定していますが、開発用の環境だからセキュリティ上の問題は無いという前提で行っています。
(7) では rails にユーザを変更しています。(8), (9) で RVM と Ruby-2.1.2 をインストール、(10) で gem をインストールするときにドキュメントを生成しないための設定を行い(ドキュメントもインストールしたいという方はコメントアウトしてください)、そして (11) で Rails-4.1.4 をインストールしています。
(12) では作業用のディレクトリとして ~/workspace を作成しています。後ほど hello という名前の Rails プロジェクトを ~/workspace/hello に作成します。
(13) では root ユーザに戻していますが、これは SSH サーバを root 権限で動かすためです。
(14) ではコンテナ外に出すポートとして 22 (SSH) と 3000 (Rails) を指定しています。
(15) では外部からマウントする場所として /home/rails/workspace を指定しています。こコンテナ内で開発したコードは git や subversion リポジトリにチェックインするので外部とファイルを共有する必要が無いという場合は不要ですが、コンテナ内のファイルをコンテナ外から簡単に取り出せるようにしておきたかったので、指定しています。
(16) ではコンテナを起動したときに実行するコマンドを指定しています。今回はコンテナには ssh 接続するので、ssh のデーモンを指定しています。
Dockerfile を作成したら、ビルドします。
1 2 |
cd <Dockefile を作成したディレクトリ> docker build -t work:0.0.0 . |
ビルドにはしばらく時間がかかりますが、完了すると work:0.0.0 というイメージが出来上がるはずです。
1 2 3 4 |
$ docker images REPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZE work 0.0.0 bfd16ab196b3 About an hour ago 661.4 MB ... |
Rails 開発用のコンテナを起動する
コンテナを起動する前に、コンテナ内にマウントするディレクトリを作成します。
1 2 |
mkdir ~/workspace chmod 777 ~/workspace |
次にコンテナを起動します。
1 2 |
docker run -d --name work --publish 80:3000 --publish 10022:22 --link postgres:db \ --volume ~/workspace:/home/rails/workspace --privileged work:0.0.0 |
オプションが多いので一つずつ説明します。
-d はコンテナをデタッチモードで起動するオプションです。コンテナで動かすのは ssh のデーモンなのでデタッチモードを選びました。bash 等のインタラクティブに操作するプロセスを起動する場合はアタッチモードで起動します。
--name work はコンテナに work という名前を付けます。名前を付けておくことで、コンテナの停止/再開を docker stop work や docker start work のように名前で指定することが出来るので便利です。使い捨てではないコンテナ(しばらく動き続けるコンテナ)には --name で名前を付けておくと良いでしょう。
--publish 80:3000 はホストの 80 ポートとコンテナ内の 3000 ポートをマッピングします。これで rails server でアプリケーションを起動すればホスト側の 80 ポートでアクセスできるようになります(3000 ポートは rails server で起動したときのデフォルトポートです)。--publish 10022:22 はホスト側の 10022 ポートとコンテナ内の 22 ポートをマッピングします。22 ポートは ssh のデフォルトポートですが、ホスト側の 22 ポートを使用すると困る(ホスト自身に外部から ssh 接続している)場合もあるので、10022 にマッピングしています。
--link postgres:db は最初の方で起動した PostgreSQL が動くコンテナとリンクするためのものです。これは postgres という名前のコンテナを db という名前で参照できるようにするという意味です(起動したコンテナ内の /etc/hosts にエントリが追加されます)。
--volume ~/workspace:/home/rails/workspace はホスト側の ~/workspace とコンテナ内の /home/rails/workspace をマウントします。
--privileged は --volume と関連するオプションです。デフォルトではコンテナ内でマウント領域に対する機能は制限されているため、ファイルの作成などを行おうとすると「Permission Denied」とエラーになってしまいます。--privileged オプションを指定すると、ホスト側と同じ権限を与えることができます。
最後の work:0.0.0 はコンテナの作成に使用するイメージです。
コンテナ内に SSH 接続する
コンテナ内に SSH で接続してみましょう。
1 |
ssh -p 10022 rails@localhost |
コンテナ内の 22 ポートはホスト側の 10022 ポートにマッピングしているため、このようになります。
接続するとパスワードを求められますが、Dockerfile 内で設定した「password」と入力すれば OK です。
これでコンテナ内で作業できるようになりました。
コンテナ内を探索する
コンテナ内を探索してみましょう。
/etc/hosts の中を見ると、--link オプションで指定したとおり db というエントリが追加されていることを確認できます。
1 2 3 |
$ cat /etc/hosts ... 172.17.1.3 db |
db サーバに接続できるか確認してみましょう。
1 |
psql -h db -U postgres |
接続できれば OK です。
データベースユーザを作成する
Rails プロジェクトを作成する前に、接続用のデータベースユーザを作成します。
1 |
createuser -h db -U postgres --createdb rails |
rails という名前で作成しました。
Rails プロジェクトを作成する
hello という名前の Rails プロジェクトを作成しましょう。
1 2 |
cd ~/workspace rails new -d postgresql hello |
プロジェクトを作成したら、データベースの接続先を設定します。
1 2 |
cd ~/workspace/hello vi config/database.yml |
database.yml を開き、「host: db」を追加します。
1 2 3 4 5 6 7 8 |
default: &default adapter: postgresql encoding: unicode # For details on connection pooling, see rails configuration guide # http://guides.rubyonrails.org/configuring.html#database-pooling pool: 5 host: db # ※追加※ ... |
次はデータベースを作成します。
1 |
rake db:create db:migrate |
アプリケーションを起動する
長い道のりでしたが、アプリケーションを起動してみましょう。
1 |
bin/rails server |
無事に起動できれば、ホスト側の「http://localhost/」でアクセスできるはずです。
思ったこと、考えたこと、試行錯誤したこと
ここまでやるのに色々と試行錯誤したので、その道のりを簡単にまとめます。
Dockerfile を作り上げるのは大変だ
Dockerfile を作る時は試行錯誤の連続でした。少し書いてはビルドして、エラーが出たら修正して・・の繰り返しになるので、ものすごく時間がかかりました。特に「このパッケージも必要だった! → Dockerfile の始めのほうに追加 → ほとんどキャッシュが効かないのでビルド時間がすごくかかる・・」ということも多々ありました。
「すごく時間がかかる・・」と気付いてから、ベースとするイメージで bash を起動し、そこでコマンドを打ちながら Dockerfile に転記していくやり方に変更しました。ある程度書き上げてから一気にビルドすればトータルの時間は短くなると思います。
開発用のコンテナで bash を動かすべきか sshd を動かすべきか(sshd にした)
開発用のコンテナ(今回の例では work)で bash を動かすべきか、sshd を起動して ssh 接続するか迷いました。最初は bash を動かすようにしていたのですが、Ctrl+p で一つ前のコマンドを出せない(デタッチするための Ctrl+p Ctrl+q とかぶっているため、一つ前のコマンドを出すには Ctrl+p Ctrl+p と 2 回入力する必要がある)ため、最終的に ssh 接続するやり方を採用しました。
開発用のコンテナでは --volume でマウントした場所に Rails プロジェクトを作成した
開発用のコンテナで Rails プロジェクトを作成しましたが、そのファイルは --volume で外部からマウントしたディレクトリに置くようにしました。そうしたことでコンテナを気軽に破棄して作り直すことができるようになりました。頻繁に作り直す可能性のあるコンテナについては、データを外出ししておいた方が良さそうです。
インストールパッケージのバージョンを厳密に指定するべきか(指定しなかった)
Dockerfile 内で「RUN apt-get install -y openssh-server」のように記述しましたが、「RUN apt-get install -y openssh-server=1:6.6p1-2ubuntu2」のようにバージョンを厳密に指定するべきか迷いました。なぜ迷ったのかというと、同じ Dockerfile でもビルドした時期によって出来上がるイメージが異なってしまう可能性があるのはどうよと思ったからです。結局「そこまで神経質にならなくても良い」と思い、バージョンは指定しないようにしました。
PostgreSQL のデータディレクトリを --volume で指定するべきか(指定しなかった)
PostgreSQL のコンテナを作成するときに --volume でデータディレクトリを指定するかどうかです。間違って PostgreSQL のコンテナを削除してしまった場合にデータが失われるリスクを考えたのですが、気にしすぎ(本番環境だったらバックアップするだろうし、手元の環境のように全部まとめて削除のようなことはしないはず)という結論になりました。
オフィシャルリポジトリはありがたいけれど、情報が少ない
オフィシャルリポジトリは非常にありがたい存在なのですが、まだ情報が少ないと感じます。起動方法以外は説明が無い場合もあります。今回は PostgreSQL のオフィシャルイメージを使ったのですが、すぐには initdb するときのエンコーディングを変更する方法が分かりませんでした。困った時は Docker library の中を見ると解決するかもしれません。
いったん動けるようになったら楽だった
試行錯誤している段階を過ぎてしまえば、コンテナを気軽に作成/破棄できるので楽だと思えるようになりました。消えては困るデータは --volume でホスト側からマウントし、コンテナを捨てやすくしておくこともポイントだと感じました。
まとめ
Enjoy Docker!