【Rails and Docker】Rails用のdocker-compose、Dockerfileを良い具合に設定してみた

## はじめに

皆さん、こんにちは。今回は久々にRails(どちらかと言うとDockerだけど、その中でRailsに特化したということで)の記事を執筆しようと思います。
今回の記事の執筆の経緯ですが直近、会社の開発環境をリプレースする仕事を行なっていました。従来、AWSのEC2インスタンス上で直接Railsを動かしていたのですが、会社のプロジェクトが増えた事もあり、開発環境で複数のプロジェクトに対応できるように、これまた複数のRails環境を構築する事も増えてきました。そのような背景があり、`複数のプロジェクトに対応することになっても、対応できるようにコンテナ型仮想化で独立したRails環境を作る`必要が出てきました。
という事で、Rails環境を動かすコンテナ群を設定するためのdocker-compose.yml、Dockerfileのベターな設定(?)を共有できたらと思います。

## コンテナ型仮想化について

こちらを参照→[Dockerチュートリアル 開発者向け](https://www.slideshare.net/TatsuyaYagi4/docker-tutorial-103853621)

## docker-compose.ymlの設定

docker-compose.ymlの設定は以下のようにしました。

```yaml:docker-compose.yml
version: '3'
volumes:
  pids:
services:
  web:
    build:
      context: .
      dockerfile: Dockerfile
    command: bundle exec rails s -p 3000 -b 0.0.0.0
    volumes:
      - .:/usr/local/rails
      - pids:/usr/local/rails/tmp/pids
    links: [db, redis]
    environment:
      - DEVELOPMENT_HOST=db
      - TEST_HOST=db
      - REDIS_URL=redis://redis:6379
      - REDIS_PORT_6379_TCP_ADDR=redis
    ports:
      - 3000:3000
    stdin_open: true
    tty: true
  db:
    image: mysql:5.7
    command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_bin --innodb-large-prefix=true --innodb-file-format=Barracuda
    environment:
      - MYSQL_ALLOW_EMPTY_PASSWORD=yes
    ports:
      - 3306:3306
  redis:
    image: redis:latest
    ports:
      - 6379:6379
```

一つ一つ、docker-compose.ymlの設定を確認していきましょう。

### docker-compose.ymlの設定を確認してみる

今回はdocker-composeの記述の説明は省略し、`Railsアプリを開発するためにどのような設定を行なったのか`という事を重点に置いて説明していきます。

```yaml
    build:
      context: .
      dockerfile: Dockerfile
    command: bundle exec rails s -p 3000 -b 0.0.0.0
```

と言いながら、いきなりdocker-compose.ymlの記述の説明です。実行するRailsコンテナのコンテナイメージをカスタムして構築するようにDockerfileで構築するように指定しています。更に、コンテナ起動時に渡す起動コマンドとしてbundle経由でのRailsサーバーの起動コマンドを渡しています。

### volumes

```yaml
volumes:
  pids:
```

```yaml
    volumes:
      - .:/usr/local/rails
      - pids:/usr/local/rails/tmp/pids
```

次にvolumesの説明です。ホスト配下のディレクトリとコンテナ配下のディレクトリをマウントしています。これにより、`ホストのファイルを変更した時に即座にRailsコンテナに変更が反映されるようにしています(勿論、再起動しなければ反映されないconfigの設定などは置き換えた後にrestartかけるようにします)。`
本来は、Railsアプリの直下のディレクトリ全部マウントしたいところですが、tmp/pidsをマウントしてしまうとコンテナの方でpidファイルが残り続けてしまいます。そうなると、Railsサーバー再起動時にpidファイルの存在を認識してしまい`pidファイルを消さない限りRailsサーバーが起動できない問題が発生したため、`コンテナ専用の空のData Volumeを作成し、tmp/pidsにマウントすることでコンテナ直下のtmp/pidsの内容をクリアにします。

### DB, Redisのリンク

```yaml
    links: [db, redis]
    environment:
      - DEVELOPMENT_HOST=db
      - TEST_HOST=db
      - REDIS_URL=redis://redis:6379
      - REDIS_PORT_6379_TCP_ADDR=redis
    ports:
      - 3000:3000
```

この辺りは単純に環境変数をあらかじめ渡してあげて接続するDBやRedisを認識しているだけです。

### binding.pryなどのデバッグ系ツールを使うために、stdin_open,ttyを有効にしておく

```yaml
    stdin_open: true
    tty: true
```

基本的にdocker-composeで動かすときは`docker-compose up -d`でコンテナ一式をデーモンモードで起動しています。その際、デバッグ系のツールを利用して、値や実行の流れを確認したい時にコンテナへ標準入力ができるようにしておく必要があります。
上記のコマンドで、標準入力の有効化、標準入力のデバイスを有効にするために、ttyをtrueにしています。
また、デーモンモードで動いているコンテナにアタッチするにはdocker ps などでコンテナIDを取得し、コンテナにattachする必要があります。

```bash
% docker ps -a
CONTAINER ID        IMAGE                    COMMAND                  CREATED             STATUS                  PORTS                               NAMES
cfbc22800121        web              bundle exec rails s -b 0.0.0.0   27 hours ago        Up 2 hours              0.0.0.0:3000->3000/tcp
% docker attach cfbc22800121
```

以上で、docker-composeの設定は終わりです。続いて、Dockerfileの設定です。

## Dockerfileの設定

rails専用のカスタムイメージを構築するために、Dockerfileスクリプトを記述していきます。
全体の内容は以下のようになります。

```Docker:Dockerfile
FROM ruby:*****

RUN apt update
# 必要なライブラリをインストール
RUN apt install -y build-essential libpq-dev apt-transport-https apt-utils libprotobuf-dev libprotoc-dev protobuf-compiler

# 日本語環境を整える
ENV LANG C.UTF-8

# システム時刻をJSTに変更
RUN rm -f /etc/localtime
RUN ln -fs /usr/share/zoneinfo/Asia/Tokyo /etc/localtime

# phantomjsをインストール
RUN apt install -y chrpath libssl-dev libxft-dev
RUN apt install -y libfreetype6 libfreetype6-dev libfontconfig1 libfontconfig1-dev
ADD https://bitbucket.org/ariya/phantomjs/downloads/phantomjs-2.1.1-linux-x86_64.tar.bz2 phantomjs-2.1.1-linux-x86_64.tar.bz2
RUN tar xvjf phantomjs-2.1.1-linux-x86_64.tar.bz2 -C /usr/local/share/
RUN ln -sf /usr/local/share/phantomjs-2.1.1-linux-x86_64/bin/phantomjs /usr/local/bin

# 最新のnpmをインストール
RUN curl -sL https://deb.nodesource.com/setup_10.x | bash -
RUN apt install -y nodejs

RUN apt install -y mysql-client

# Chrome & ChromeDriverインストール
RUN apt-get install -y unzip && \
    CHROME_DRIVER_VERSION=`curl -sS chromedriver.storage.googleapis.com/LATEST_RELEASE` && \
    wget -N http://chromedriver.storage.googleapis.com/$CHROME_DRIVER_VERSION/chromedriver_linux64.zip -P ~/ && \
    unzip ~/chromedriver_linux64.zip -d ~/ && \
    rm ~/chromedriver_linux64.zip && \
    chown root:root ~/chromedriver && \
    chmod 755 ~/chromedriver && \
    mv ~/chromedriver /usr/bin/chromedriver && \
    sh -c 'wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add -' && \
    sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google-chrome.list' && \
    apt-get update && apt-get install -y google-chrome-stable

# 日本語フォント設定
RUN mkdir /noto
ADD https://noto-website.storage.googleapis.com/pkgs/NotoSansCJKjp-hinted.zip /noto
WORKDIR /noto
RUN unzip NotoSansCJKjp-hinted.zip && \
    mkdir -p /usr/share/fonts/noto && \
    cp *.otf /usr/share/fonts/noto && \
    chmod 644 -R /usr/share/fonts/noto/ && \
    /usr/bin/fc-cache -fv

WORKDIR /
RUN rm -rf /noto

RUN mkdir -p /usr/local/rails

# bundle installのキャッシュ（詳細は後述）
WORKDIR /usr/local/rails
ADD Gemfile Gemfile
ADD Gemfile.lock Gemfile.lock
ADD package.json package.json
ADD config/database.yml config/database.yml
RUN bundle install
```

Dockerfileの中でやっている事は、ほぼコメントに残しているのですが、一つ一つ解説していきます。

### Rails実行前のインストール作業

```Docker
FROM ruby:*****

RUN apt update
# 必要なライブラリをインストール
RUN apt install -y build-essential libpq-dev apt-transport-https apt-utils libprotobuf-dev libprotoc-dev protobuf-compiler
```

この部分はデフォルトでrubyが入ったコンテナイメージ(*****はversionを指定する)をRailsが動くようにしたいのでパッケージを入れます。コンテナのOSはDebianなため、aptリポジトリからパッケージをダウンロードします。

### 日本語環境やシステム時刻の設定

```Docker
# 日本語環境を整える
ENV LANG C.UTF-8

# システム時刻をJSTに変更
RUN rm -f /etc/localtime
RUN ln -fs /usr/share/zoneinfo/Asia/Tokyo /etc/localtime
```

`rails c`の日本語入力、また`RSpec`における時刻によるテスト事例を正しく検証するために、LANG環境変数をC.UTF-8に指定し、更に、コンテナの/etc/localtimeにホストのJSTのバイナリファイルのシンボリックリンクを作ってあげます。

### ブラウザパッケージのインストール

```Docker
# phantomjsをインストール
RUN apt install -y chrpath libssl-dev libxft-dev
RUN apt install -y libfreetype6 libfreetype6-dev libfontconfig1 libfontconfig1-dev
ADD https://bitbucket.org/ariya/phantomjs/downloads/phantomjs-2.1.1-linux-x86_64.tar.bz2 phantomjs-2.1.1-linux-x86_64.tar.bz2
RUN tar xvjf phantomjs-2.1.1-linux-x86_64.tar.bz2 -C /usr/local/share/
RUN ln -sf /usr/local/share/phantomjs-2.1.1-linux-x86_64/bin/phantomjs /usr/local/bin

# 最新のnpmをインストール
RUN curl -sL https://deb.nodesource.com/setup_9.x | bash -
RUN apt install -y nodejs
RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add -

RUN apt install -y mysql-client

# Chrome & ChromeDriverインストール
RUN apt-get install -y unzip && \
    CHROME_DRIVER_VERSION=`curl -sS chromedriver.storage.googleapis.com/LATEST_RELEASE` && \
    wget -N http://chromedriver.storage.googleapis.com/$CHROME_DRIVER_VERSION/chromedriver_linux64.zip -P ~/ && \
    unzip ~/chromedriver_linux64.zip -d ~/ && \
    rm ~/chromedriver_linux64.zip && \
    chown root:root ~/chromedriver && \
    chmod 755 ~/chromedriver && \
    mv ~/chromedriver /usr/bin/chromedriver && \
    sh -c 'wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add -' && \
    sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google-chrome.list' && \
    apt-get update && apt-get install -y google-chrome-stable

# 日本語フォント設定
RUN mkdir /noto
ADD https://noto-website.storage.googleapis.com/pkgs/NotoSansCJKjp-hinted.zip /noto
WORKDIR /noto
RUN unzip NotoSansCJKjp-hinted.zip && \
    mkdir -p /usr/share/fonts/noto && \
    cp *.otf /usr/share/fonts/noto && \
    chmod 644 -R /usr/share/fonts/noto/ && \
    /usr/bin/fc-cache -fv

WORKDIR /
RUN rm -rf /noto
```

この部分では、Feature Spec、System Specのテストを実行できるようにHeadless BrowserのPhantomjs、およびGoogle Chromeを入れています。まだ、System Specの方はテストシナリオを作成していませんが、今後のサポートのためにあらかじめ入れておきます。

```Docker
RUN mkdir -p /usr/local/rails

# bundle installのキャッシュ（詳細は後述）
WORKDIR /usr/local/rails
ADD Gemfile Gemfile
ADD Gemfile.lock Gemfile.lock
ADD package.json package.json
ADD config/database.yml config/database.yml
RUN bundle install
```

最後にRailsを起動プロセスとしているのでRailsが起動できるようにbundle installが行えるまでの下準備を設定しておきます。
このスクリプトでコンテナイメージを作成し、コンテナ群を起動します。

```bash
% docker-compose build
% docker-compose up -d
```

## その後は

migrateやnpm run buildなどはホスト経由で行うようにします。`コンテナイメージを作成するために、及びRailsサーバーを起動するのに必要でないものは後で行うところがミソです。`

```bash
% docker exec web_1 bundle exec rake db:create
% docker exec web_1 bundle exec rake db:migrate
% docker exec web_1 bundle exec rake db:seed
% docker exec web_1 npm install
% docker exec web_1 npm run build
```

## まとめ

ちょっとRailsコンテナの作り方に慣れすぎたせいでこんな記事を書いてみました。
が、他のプラットフォームやフレームワークでのベストプラクティスな環境も作っていきたいと思うので、またどこかで、違う言語でのベストプラクティスなコンテナ構築の記事も書いていきたいですね。それでは。

## 参考文献

今回はなし