Dockerで快適なRails開発環境を手に入れるためにやった6つのこと
はじめに
この記事はDockerその2アドベントカレンダーの14日目です。
DockerでRails開発環境を作るに当たってしたことについて書きます。
普通のRails環境構築については他に記事がいっぱいあるのでそちらを見てください。
なお、現在RailsでAPIサーバを書いているのでフロントエンドでのみ必要になることについては触れません。
また、今回の記事を書くにあたってリポジトリをgithubで公開しています。
https://github.com/yuemori/docker-adventcalendar-example
アジェンダ
- Dockerfileとdocker-composeを環境毎に分ける
- data volume containerを使ってデータをキャッシュする
- entrykit, dockerizeでDBの起動待ちをする
- ラッパースクリプトを書く
- springを使えるようにする
- ビルドタグをつける
Dockerfileとdocker-composeを環境毎にわける
まず前提として、開発用・本番用・テスト用のDockerfileとdocker-composeをそれぞれ作ります。
Dockerで開発環境を作る場合、volumeを使うことでリアルタイムにコンテナ側に変更が伝わるためvolumeを使ったほうが便利です。
しかし、productionやtest環境ではvolumeを使わない方がベストプラクティスと言えます。
docker-composeを分けることで docker-compose -f
でdocker-composeファイルを指定することで、任意の環境のdockerクラスタを起動する事ができるようになります。
また、本番ではデータベースはコンテナを使っておらずdevelopmentやtestとは構成が異なるなど、環境毎の事情を吸収するのも分ける理由の一つです。
なお、production用のDockerfileやdocker-composeについては本記事では扱わないため触れません。
development
developmentはvolumeを使う前提で記述していきます。
FROM ruby:2.3.3
ENV APP_ROOT /usr/src/app
WORKDIR $APP_ROOT
CMD ["rails", "server", "-b", "0.0.0.0", "--pid", "/tmp/server.pid"]
version: '2'
services:
database:
container_name: myrails-development-database
image: mysql:5.7
environment:
MYSQL_ALLOW_EMPTY_PASSWORD: 'yes'
ports:
- 3306:3306
rails:
image: myrails-development
container_name: myrails-development-rails
build:
context: .
dockerfile: Dockerfile.development
environment:
RAILS_ENV: development
DATABASE_URL: mysql2://root@database:3306/sample_database
volumes:
- .:/usr/src/app
ports:
- 3000:3000
test
test用のDockerfileはほぼproductionと同じものになりますが、productionと違いデータベースコンテナを使った方がCIが楽なので、基本的な部分を書いていきます。
FROM ruby:2.3.1
ENV APP_ROOT /usr/src/app
WORKDIR $APP_ROOT
COPY Gemfile Gemfile
COPY Gemfile.lock Gemfile.lock
RUN bundle install -j4
COPY . $APP_ROOT
EXPOSE 3000
CMD ["rspec"]
version: '2'
services:
database:
container_name: myrails-test-database
image: mysql:5.7
environment:
MYSQL_ALLOW_EMPTY_PASSWORD: 'yes'
rails:
container_name: myrails-test-rails
image: myrails-test
build:
context: ../ # ディレクトリを掘っているため
dockerfile: Dockerfile.test
environment:
DATABASE_URL: mysql2://root@database:3306
解説
ポイントとして、テスト用のdocker-composeはルートディレクトリではなく dockerfiles
などのディレクトリを作ってそこに設置します。
docker-composeはdocker-compose.ymlが置いてあるディレクトリをプロジェクトとして扱い、コンテナの管理をします。
そのため、development用とtest用のファイルを両方ルートディレクトリにおいておくとテストの実行でコンテナを起動するとdevelopment用のクラスタが停止させられるなど影響が出てしまいます。
-pオプションでプロジェクト名を指定すると切り替えることが出来ますが、面倒なのでディレクトリ自体を変更します。
(コンテナ名を指定して起動しても駄目だったので、もっと良い方法があったら教えてください!)
data volume containerを使ってデータをキャッシュする
このままではdevelopmentではコンテナを起動するたびに bundle install
や db:migrate
が必要になってしまうので、データをキャッシュするようにしていきます。
データのキャッシュにはdata volume containerを使います。
data volume containerを使ってデータをキャッシュすることで、例えば開発用にgemを追加したときなどの際にbundle installをフルで行わなくてよくなるなどのメリットがあります。
実際に開発してると、レビューなどの際にgemを追加したブランチに切り替えてbuild→自分のブランチに戻ってbuildなどで毎回フルインストールが走るのが地味にめんどくさいです。
data volume containerの追加
version: '2'
services:
+ datastore:
+ container_name: myrails-development-datastore
+ image: busybox
+ volumes:
+ - bundle_install:/usr/local/bundle
+ - mysql:/var/lib/mysql
+
database:
container_name: myrails-development-database
image: mysql:5.7
environment:
MYSQL_ALLOW_EMPTY_PASSWORD: 'yes'
+ volumes_from:
+ - datastore
ports:
- 3306:3306
rails:
container_name: myrails-development-rails
image: myrails-development
build:
context: .
dockerfile: Dockerfile.development
command: rails server -b 0.0.0.0
environment:
RAILS_ENV: development
DATABASE_URL: mysql2://root@database:3306
+ volumes_from:
+ - datastore
volumes:
- .:/usr/src/app
depends_on:
- database
ports:
- 3000:3000
+
+ volumes:
+ mysql:
+ driver: local
+ bundle_install:
+ driver: local
解説
testではクリーンな環境にするためにdatabaseコンテナを毎回破棄しているので、data volume containerを使っていません。
bundle_installのvolume先が /usr/local/bundle
なのは、Dockerhubのruby公式イメージのインストール先がそこのためです(この辺のこと)。
.bundle
なども同じところにおいてあるため、そこをキャッシュします。
entrykit, dockerizeでDBの起動待ちをする
Dockerで気をつけないといけないことの一つに、コンテナの起動=サービスの起動ではない点があります。
特に開発環境やテスト実行時には、migrationの際にデータベースがまだ起動しておらず失敗するケースがあります。
解決策の一つとして docker-compose up -d database
などとして先に起動してからsleepしてmigrationを行うというのがありますが、entrykitとdockerizeを使うことでそこを簡略化・自動化することが出来ます。
entrykitについてはEntrykitのすすめという非常にわかりやすい記事があるのでそちらを参考にしてください。
dockerizeについては記事が少ないので少し解説します。
dockerizeは公式のControlling startup order in Composeでも紹介されているツールで、applicationのdocker化(dockerize)をする際に便利な機能を提供してくれるものです。
機能がいくつかありますが、その1つに「tcp connectionをチェックして応答を待つ」機能があるのでそれを利用します。
development
FROM ruby:2.3.3
ENV APP_ROOT /usr/src/app
+ENV DOCKERIZE_VERSION v0.3.0
+ENV ENTRYKIT_VERSION 0.4.0
+
+RUN apt-get update && apt-get install -y wget ca-certificates openssl \
+ # dockerize
+ && wget https://github.com/jwilder/dockerize/releases/download/$DOCKERIZE_VERSION/dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz \
+ && tar -C /usr/local/bin -xzvf dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz \
+ && rm dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz \
+ # entrykit
+ && wget https://github.com/progrium/entrykit/releases/download/v${ENTRYKIT_VERSION}/entrykit_${ENTRYKIT_VERSION}_Linux_x86_64.tgz \
+ && tar -xvzf entrykit_${ENTRYKIT_VERSION}_Linux_x86_64.tgz \
+ && rm entrykit_${ENTRYKIT_VERSION}_Linux_x86_64.tgz \
+ && mv entrykit /bin/entrykit \
+ && chmod +x /bin/entrykit \
+ && entrykit --symlink
WORKDIR $APP_ROOT
+
+ENTRYPOINT [ \
+ "prehook", "bundle install -j3", "--", \
+ "prehook", "dockerize -timeout 60s -wait tcp://database:3306", "--" \
+]
rails:
image: myrails-development
//...
environment:
RAILS_ENV: development
DATABASE_URL: mysql2://root@database:3306/sample_database
+ depends_on:
+ - database
volumes_from:
- datastore
volumes:
test
ENV APP_ROOT /usr/src/app
+ENV DOCKERIZE_VERSION v0.3.0
+ENV ENTRYKIT_VERSION 0.4.0
+
+RUN apt-get update && apt-get install -y wget ca-certificates openssl \
+ # dockerize
+ && wget https://github.com/jwilder/dockerize/releases/download/$DOCKERIZE_VERSION/dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz \
+ && tar -C /usr/local/bin -xzvf dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz \
+ && rm dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz \
+ # entrykit
+ && wget https://github.com/progrium/entrykit/releases/download/v${ENTRYKIT_VERSION}/entrykit_${ENTRYKIT_VERSION}_Linux_x86_64.tgz \
+ && tar -xvzf entrykit_${ENTRYKIT_VERSION}_Linux_x86_64.tgz \
+ && rm entrykit_${ENTRYKIT_VERSION}_Linux_x86_64.tgz \
+ && mv entrykit /bin/entrykit \
+ && chmod +x /bin/entrykit \
+ && entrykit --symlink
WORKDIR $APP_ROOT
COPY . $APP_ROOT
EXPOSE 3000
+ENTRYPOINT [ \
+ "prehook", "dockerize -timeout 60s -wait tcp://database:3306", "--"
+]
+
CMD ["rspec"]
build:
context: ../ # ディレクトリを掘っているため
dockerfile: Dockerfile.test
+ depends_on:
+ - database
environment:
RAILS_ENV: test
DATABASE_URL: mysql2://root@database:3306/sample_database
解説
DockerfileではENTRYPOINTにentrykitのprehookを利用してdockerizeでの起動待ちを仕込み、docker-composeで depends_on
しておきます。
こうしてdatabaseコンテナを先に起動させてからdockerizeで起動待ちをすることで、起動コマンドを docker-compose up
に集約させたり、コマンド実行の際にdatabaseコンテナが起動してるかを気にせずに docker-compose run
などを行うことが出来るようになります。
以下は起動待ちしている様子です。
また、 Dockerfile.test
でprehookでdb:createなどを行っても良いですが、テスト時にrubocopなどを実行する際に毎回 db:create db:schema:load
が走ると時間がかかってしまうので、prehookではなくテストスクリプト内で実行するようにします。
ラッパースクリプトを書く
毎回 docker-compose run --rm rails hogehoge
などとしているとめんどくさいです。
特にテスト用は docker-compose -f dockerfiles/docker-compose.test.yml --rm rails
で履歴から引っ張ってくるのもめんどいですしちょっとやってられませんのでラッパースクリプトを書きます。
ラッパースクリプトを書くことで、チーム内でのdocker-composeの取扱いを共通化出来たり、習熟度の低いメンバーには抽象化レイヤとして機能するなどのメリットがあります。
今回はbin/sample
としてシェルスクリプトを追加します。
設置場所やスクリプト名などは適宜変えてください。アプリケーション名にするとスクリプトにも愛着が湧くかも。
wrapper script
ちょっと長いので適宜読み飛ばしてください:)
プロジェクトで使ってるやつを参考に新規で書きましたが、プロジェクトではrubocopの実行やannotateの差分チェックなどがテストに追加されていたり、ビルド前に必要なものの生成などがここに入ってきます。
#!/bin/bash
set -eu
RAILS_ROOT=$(cd $(dirname $0)/../ && pwd)
DEV_COMPOSE_FILE="${RAILS_ROOT}/docker-compose.yml"
TEST_COMPOSE_FILE="${RAILS_ROOT}/dockerfiles/docker-compose.test.yml"
function usage() {
cat <<EOF
Usage:
$(basename ${0} [command])
Commands:
init 初期化する
build イメージをビルドする
clean コンテナをすべて破棄する
start サーバをバックグラウンドで起動する
stop サーバを停止する
status コンテナの起動状況を見る
restart サーバを再起動する
logs バックグラウンドで起動しているサーバのログを見る
up サーバをフォアグラウンドで起動する
test テストを実行する
EOF
}
function build() {
docker-compose build
}
function init() {
docker-compose down --volumes --remove-orphans --rmi all
docker-compose build
docker-compose run --rm rails rails db:create db:schema:load
}
function clean() {
docker-compose stop
docker-compose rm --force
docker-compose down --volumes --remove-orphans --rmi all
docker-compose -f $TEST_COMPOSE_FILE stop
docker-compose -f $TEST_COMPOSE_FILE rm --force
docker-compose -f $TEST_COMPOSE_FILE down --volumes --remove-orphans --rmi all
}
function start() {
docker-compose up -d rails
}
function stop() {
docker-compose stop rails
}
function logs() {
docker-compose logs rails
}
function restart() {
start
stop
}
function status() {
docker-compose ps
}
function up() {
docker-compose up rails
}
function run_test() {
export COMPOSE_FILE=$TEST_COMPOSE_FILE
docker-compose down --volumes --remove-orphans
docker-compose build
docker-compose run --rm rails rails db:create db:schema:load
docker-compose run --rm rails rspec
}
shift `expr $OPTIND - 1`
if [ $# -eq 0 ];then
usage
exit 1
fi
export COMPOSE_FILE=$DEV_COMPOSE_FILE
case "${1}" in
"init")
init
;;
"build")
build
;;
"clean")
clean
;;
"start")
start
;;
"stop")
stop
;;
"status")
status
;;
"restart")
restart
;;
"logs")
logs
;;
"up")
up
;;
"test")
run_test
;;
*)
usage
exit 1
;;
esac
解説
このscriptを追加することで bin/sample init
してから bin/sample start
するだけでサーバの起動を行うことが出来ます。
別にthorとか使ってrubyで書いても良いんですが、やりたいことに対してちょっと大げさすぎるのでシェルで書いてます。
テスト実行はここじゃなくて別にスクリプトを追加しても良いですが、なにかしらまとめておくことでテスト実行のプロセスをチームで共通化出来て良いと思います。
pre-pushのgit hookを用意して bin/sample test
しとくとかのアプローチも有効ですね。
springを有効にする
rails開発で便利なspringですが、このままだと使うことが出来ません。
そこで、spring用のサービスを追加します。
springサービスを追加する
ports:
- 3306:3306
- rails:
+ rails: &rails
container_name: myrails-development-rails
image: myrails-development
build:
context: .
dockerfile: Dockerfile.development
- environment:
+ environment: &rails_environment
RAILS_ENV: development
DATABASE_URL: mysql2://root@database:3306/sample_database
depends_on:
- database
volumes_from:
- datastore
volumes:
- .:/usr/src/app
ports:
- 3000:3000
+ tty: true
+ stdin_open: true
+
+ spring:
+ <<: *rails
+ container_name: myrails-development-spring
+ command: spring server
+ environment:
+ <<: *rails_environment
+ SPRING_SOCKET: /tmp/spring.sock
+ ports: []
volumes:
mysql:
restart サーバを再起動する
logs バックグラウンドで起動しているサーバのログを見る
up サーバをフォアグラウンドで起動する
+ exec springコンテナでコマンドを実行する
test テストを実行する
EOF
}
@@ -62,6 +63,34 @@ function restart() {
stop
}
+function execute_command() {
+ local pid=$(docker-compose ps -q spring)
+
+ if [ -z "$pid" -o $(docker ps --quiet --filter id=${pid} | wc -l) -eq 0 ];then
+ docker-compose up -d spring
+
+ local message="waiting for spring start"
+
+ while true
+ do
+ log=$(docker-compose logs spring | grep "started on /tmp/spring.sock" | wc -l)
+
+ if [ $log -ge 1 ];then
+ echo "spring the started!"
+ break;
+ fi
+
+ message="$message."
+ echo -e "\r$message\c"
+
+ sleep 1
+ done
+ fi
+
+ docker-compose exec spring $@
+ exit $?
+}
+
function status() {
docker-compose ps
}
@@ -129,6 +158,12 @@ case "${1}" in
run_test
;;
+ "exec")
+ shift
+
+ execute_command $@
+ ;;
+
*)
usage
exit 1
解説
ttyとstdin_openをtrueにしているのは、 binding.pry
などを使うのに必要なためです。
(詳しくはこちらのStackOverFlowの回答を参考に)
springのサービスはrailsとほぼ同じ構成ですが、起動コマンドが spring server
になっている点、portのバインドを行っていない点が異なります。
docker-compose up -d spring
で起動して docker-compose exec spring rails console
などとするとspringコンテナ上でコマンドが実行されます。
bundle install
などもこっちでやったほうが楽です。 guard
を使う場合もspringコンテナ上で起動します。
めんどいのでスクリプト側で自動化して bin/sample exec rails console
などで実行できるようにしていますが、起動確認のところが苦しいので良い感じにする方法があったら教えてください…
ビルドタグをつけやすくする
最後に、ビルドタグをつけやすくします。
ビルドタグをつけやすくすることで、CIなどでイメージやコンテナの切り替えを楽にします。
CIでイメージやコンテナの破棄で悲惨なことにならないように必要なやつです。
少し工夫すると並列実行などにも応用できますが、ここでは触れないでおきます。
(というかまだ並列実行への対応が出来ていないので、こうしたら良さそうとかしか話せません…)
${BUILD_TAG}を追加する
DEV_COMPOSE_FILE="${RAILS_ROOT}/docker-compose.yml"
TEST_COMPOSE_FILE="${RAILS_ROOT}/dockerfiles/docker-compose.test.yml"
+export BUILD_TAG=${BUILD_TAG:-'latest'}
+
function usage() {
cat <<EOF
Usage:
version: '2'
services:
datastore:
- container_name: myrails-development-datastore
+ container_name: myrails-development-datastore-${BUILD_TAG}
image: busybox
volumes:
- mysql:/var/lib/mysql
- bundle_install:/usr/local/bundle
database:
- container_name: myrails-development-database
+ container_name: myrails-development-database-${BUILD_TAG}
image: mysql:5.7
environment:
MYSQL_ALLOW_EMPTY_PASSWORD: 'yes'
@@ -18,8 +18,8 @@ services:
- 3306:3306
rails: &rails
- container_name: myrails-development-rails
- image: myrails-development
+ container_name: myrails-development-rails-${BUILD_TAG}
+ image: myrails-development:${BUILD_TAG}
build:
context: .
dockerfile: Dockerfile.development
@@ -39,7 +39,7 @@ services:
spring:
<<: *rails
- container_name: myrails-development-spring
+ container_name: myrails-development-spring-${BUILD_TAG}
command: spring server
environment:
<<: *rails_environment
version: '2'
services:
database:
- container_name: myrails-test-database
+ container_name: myrails-test-database-${BUILD_TAG}
image: mysql:5.7
environment:
MYSQL_ALLOW_EMPTY_PASSWORD: 'yes'
rails:
- container_name: myrails-test-rails
- image: myrails-test
+ container_name: myrails-test-rails-${BUILD_TAG}
+ image: myrails-test:${BUILD_TAG}
build:
context: ../
dockerfile: Dockerfile.test
解説
特に解説することもないですね…。
変数名が $BUILD_TAG
なのは、jenkinsの環境変数を流用するためです。
こちらのissueを参考にしました
ちなみに、docker-compose 1.9.0から対応しているv2.1formatではデフォルト値を設定できるようになったみたいです
(早くDocker for macも対応してくれないかな……)
まとめ
以上、Dockerで快適なRails開発環境を手に入れるためにやった6つのことでした。
「こっちではこんな風にしてる」とか「こういうときどうしてる?」とかあれば気軽にコメントください。
参考
参考にさせていただいた記事の皆様にはこの場を借りてお礼申し上げます。ありがとうございます
その他
なんでdownとかrmとかに執拗に--volumes指定してるの?
CIで自動的に作成されたdocker volumeがJenkinsスレーブのディスク容量を枯渇させるという事件があったので毎回消すようにしてます。
guardがなんか動くけど変更を検知してくれないんだけど…
guard --force-polling
すれば動きます。