dockerでrailsを動かすときの構成はどうするべきか

はじめに

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

お試しバージョン

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

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

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"]

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

#!/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

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

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する前に、GemfileGemcile.lockだけをADDしてbundle installしている箇所です。
これによって、Gemfileに更新があった場合のみbundle installが実行されるようになります。

.dockerignore

実はこの構成にした直後、「ローカルでdocker buildしたときにvendor/bundle以下にインストールされたgemのファイル群までADDされて無駄」、「Dockerfileの中で.bundle/configを消さないと設定を引き継いでしまう」などの不満点もあり、gitignoreファイルみたいに除外指定したいなーと思っていたら、docker 1.1.0で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コンテナは分離した方がいいですね。