railsをdockerで動かしたい場合の構成はどうするべきか

# はじめに

dockerでrailsを動かす場合にどうするのが良いかなーと試行錯誤し、構成も落ち着いてきたのでまとめます。

# お試しバージョン

一番最初はとりあえずってことで、railsリポジトリ + railsを動作させるコンテナの組み合わせで試してみました。

## Dockerfileの内容

```dockerfile
FROM base

# rubyインストールに必要なパッケージを用意
RUN apt-get update
RUN apt-get install -y --force-yes build-essential curl git zlib1g-dev libssl-dev libreadline-dev libyaml-dev libxml2-dev libxslt-dev

# rbenv, ruby-buildをインストール
RUN git clone https://github.com/sstephenson/rbenv.git /root/.rbenv
RUN git clone https://github.com/sstephenson/ruby-build.git /root/.rbenv/plugins/ruby-build
RUN ./root/.rbenv/plugins/ruby-build/install.sh
ENV PATH /root/.rbenv/bin:$PATH
RUN echo 'eval "$(rbenv init -)"' >> /etc/profile.d/rbenv.sh # or /etc/profile
RUN echo 'eval "$(rbenv init -)"' >> .bashrc

# rubyインストール（バージョン指定は ruby-versions.txt に）
ENV CONFIGURE_OPTS --disable-install-doc
ADD ./ruby-versions.txt /root/ruby-versions.txt
RUN xargs -L 1 rbenv install < /root/ruby-versions.txt

# bundlerを各rubyに用意
RUN echo 'gem: --no-rdoc --no-ri' >> /.gemrc
RUN bash -l -c 'for v in $(cat /root/ruby-versions.txt); do rbenv global $v; gem install bundler; done'

# sqlite3をインストール
RUN apt-get install -y sqlite3 libsqlite3-dev

# Node.jsインストール
RUN apt-get install -y software-properties-common
RUN apt-get update
RUN add-apt-repository -y ppa:chris-lea/node.js
RUN echo "deb http://archive.ubuntu.com/ubuntu precise universe" >> /etc/apt/sources.list
RUN apt-get update
RUN apt-get install -y nodejs

# ssh準備
RUN mkdir /root/.ssh/

# private keyをコピー
ADD id_rsa /root/.ssh/id_rsa

# known_hosts設定
RUN touch /root/.ssh/known_hosts
# Add bitbuckets key
RUN ssh-keyscan bitbucket.org >> /root/.ssh/known_hosts

# railsアプリケーションをclone
RUN mkdir -p /var/www
RUN git clone git@github.com:k-shogo/rails4-ajax-sample.git /var/www/rails
WORKDIR /var/www/rails

# railsのセットアップ
RUN bash -l -c 'bundle install'
RUN bash -l -c 'bundle exec rake db:create; bundle exec rake db:migrate'
RUN bash -l -c 'bundle exec rake master:import'
ADD ./secrets.yml /var/www/rails/config/secrets.yml

EXPOSE 3000
CMD bash -l -c 'bundle exec rails s'
```

## イケてないポイント

動作はするわけですが、`docker build`するたびにrubyからビルドし直すのは当然イケてないので早々に止めました。

# 試行錯誤バージョン

毎回rubyをビルドし直すのが時間がかかるので、rubyが入ったコンテナのリポジトリを分離し、
ruby, node等を用意したコンテナ + railsリポジトリ + railsが動作するコンテナ の3リポジトリ構成にしてみました。  
つまり先ほどのDockerfileを半分に分割する感じです。  
さらにrailsのセットアップ部分で`bash -l -c `とかつけるのもイケてないと感じたので、セットアップのスクリプトを分離しても見ました。

## railsを動作させるコンテナのDockerfile

```dockerfile
FROM kshogo/docker-rails-base

# Make ssh dir
RUN mkdir /root/.ssh/

# Copy over private key, and set permissions
ADD id_rsa /root/.ssh/id_rsa

# Create known_hosts & Add bitbuckets key
RUN \
  touch /root/.ssh/known_hosts && \
  ssh-keyscan bitbucket.org >> /root/.ssh/known_hosts

# install rails application
ADD ./setup.sh /root/setup.sh
RUN chmod +x /root/setup.sh
RUN /root/setup.sh

# Add configuration files in repository to filesystem
ADD ./rails/config/secrets.yml         /var/www/rails/config/secrets.yml
ADD ./start-server.sh                  /usr/bin/start-server
RUN chmod +x /usr/bin/start-server

WORKDIR /var/www/rails
EXPOSE 3000
# ENTRYPOINT ["/usr/bin/start-server"]
CMD ["/usr/bin/start-server"]
```

## セットアップのスクリプト

```bash
#!/bin/bash
source /etc/profile.d/rbenv.sh
mkdir -p /var/www
git clone git@github.com:k-shogo/rails4-ajax-sample.git /var/www/rails
cd /var/www/rails
bundle install --without test development --path vendor/bundle
bundle exec rake db:create RAILS_ENV=production
bundle exec rake db:migrate RAILS_ENV=production
bundle exec rake master:import RAILS_ENV=production
bundle exec rake assets:precompile RAILS_ENV=production
```

`source /etc/profile.d/rbenv.sh`しておけばいいじゃんってことです。

## この構成でも不満

たしかに毎回rubyビルドする部分は解消されたのですが、気になるポイントも出てきました。

* 毎回`git clone`する、つまりrailsリポジトリ変更したらpushしてからコンテナビルドし直さなきゃいけない
* ブランチ切り替えて試したいときにわざわざDockerfile編集？
* セットアップのロジックをrailsリポジトリじゃなくて動作させる側のコンテナに持たせるのが不自然

# 現在の構成

そもそもDockerでrbenvいらないじゃん + railsリポジトリの中にDockerfileがあった方がいいねってことで、
ruby, node等を用意したコンテナ + railsリポジトリ（Dockerfile同梱） の2リポジトリ構成にしてみました。  

## ベースとなるコンテナのDockerfile

```dockerfile
FROM dockerfile/ubuntu

RUN apt-get update && \
  DEBIAN_FRONTEND=noninteractive apt-get upgrade -y && \
  DEBIAN_FRONTEND=noninteractive apt-get -y install \
    build-essential \
    curl \
    git-core \
    libcurl4-openssl-dev \
    libreadline-dev \
    libssl-dev \
    libxml2-dev \
    libxslt1-dev \
    libyaml-dev \
    zlib1g-dev && \
    curl -O http://ftp.ruby-lang.org/pub/ruby/2.1/ruby-2.1.2.tar.gz && \
    tar -zxvf ruby-2.1.2.tar.gz && \
    cd ruby-2.1.2 && \
    ./configure --disable-install-doc && \
    make && \
    make install && \
    cd .. && \
    rm -r ruby-2.1.2 ruby-2.1.2.tar.gz && \
    echo 'gem: --no-document' > /usr/local/etc/gemrc

# Install Bundler for each version of ruby
RUN \
  echo 'gem: --no-rdoc --no-ri' >> /.gemrc && \
  gem install bundler

# install sqlite3
RUN apt-get install -y sqlite3 libsqlite3-dev

# Install Node.js and npm
RUN \
  apt-get install -y software-properties-common && \
  apt-get update && \
  add-apt-repository -y ppa:chris-lea/node.js && \
  echo "deb http://archive.ubuntu.com/ubuntu precise universe" >> /etc/apt/sources.list && \
  apt-get update && \
  apt-get install -y nodejs
```

もう`source /etc/profile.d/rbenv.sh`やら`bash -l -c`とかいりません

## railsリポジトリに同梱するDockerfile

```dockerfile
FROM kshogo/docker-rails-base

RUN mkdir /myapp
WORKDIR /myapp

ADD Gemfile Gemfile
ADD Gemfile.lock Gemfile.lock
RUN bundle config without test development doc
RUN bundle install
ADD . /myapp

ENV RAILS_ENV production
RUN bundle exec rake db:create
RUN bundle exec rake db:migrate
RUN bundle exec rake assets:precompile

# Add configuration files in repository to filesystem
ADD dotenv          /myapp/.env
ADD start-server.sh /usr/bin/start-server
RUN chmod +x /usr/bin/start-server

EXPOSE 3000
ENTRYPOINT ["/usr/bin/start-server"]

```

ポイントはrailsアプリ全体をADDする前に、`Gemfile`と`Gemfile.lock`だけをADDして
`bundle install`している箇所です。
これによって、Gemfileに更新があった場合のみ`bundle install`が実行されるようになります。

## .dockerignore

実はこの構成にした直後、「ローカルで`docker build`したときに`vendor/bundle`以下にインストールされたgemのファイル群までADDされて無駄」、「Dockerfileの中で`.bundle/config`を消さないと設定引き継いじゃう」などの不満点もあり、gitignoreファイルみたいに除外指定したいなーと思っていたら、
[docker 1.1.0](http://blog.docker.com/2014/07/announcing-docker-1-1/)でdockerignoreファイルに対応されたのでその不満も解消しました。

```
### Rails ###
public/assets/*

## Environment normalisation:
.bundle
vendor/bundle
```

## ポイント

* ADDがキャッシュされるのでGemfileが変更されてなければ`bundle install`がスキップされる
* `git checkout`すればブランチ違いでのbuildが簡単

# まとめ

railsリポジトリにDockerfileを同梱した方が簡単だったというのが感想です。
dockerの開発も活発で、ほしいなーと思った機能が早速docker 1.1.0に含まれていたのには感心しました。

今回はお試しということでrailsコンテナの中にsqliteでDBも内蔵してしまっていますが、
本来はDBコンテナは分離した方がいいですね。

追記（2015/02/23）
DBやその他連携するサービスを複数のコンテナで動作させるパターンを"[複雑な構成のrailsアプリをdockerで動かしたい場合はどうするべきか](http://qiita.com/k-shogo/items/3c887cbf86b3cab85b72)"にまとめました。
railsを動作させるDockerfileの記述も更新されています。