dockerでRails5+postgres+redisの開発環境を丁寧に構築してみた

## <i class="fa fa-cubes"></i> はじめに
こんにちは、OthloTechの@pokohideです。

この記事は[OthloTech Advent Calendar 2016](http://qiita.com/advent-calendar/2016/othlotech)の最終日(25日)の記事です！

世間はクリスマスムードで、こんな記事を読んでいる人もいないとは思いますが...最近MacBookProを購入して、新しいのを買ったら環境汚染しないぞ！と思っていたので、これを機に勉強してみました。なので、ステップごとに自分なりの解説コメント等付きますが、ご了承ください。

## <i class="fa fa-cubes"></i> 事前知識

### <i class="fa fa-cube"></i> `docker`とは

![docker.png](https://qiita-image-store.s3.amazonaws.com/0/85665/a5cbd877-5186-6cb5-3ca6-49ac3de3041d.png)

[Docker Inc.](https://www.docker.com/)が提供している仮想化のためのオープンソースソフトウェアです。

<img width="512" alt="dd.png" src="https://qiita-image-store.s3.amazonaws.com/0/85665/ac6e540f-fd49-3f04-2752-4b7ec846739b.png">
『Docker実戦活用ガイド』p.7 より

Vagrantなどの仮想マシンとは違って、dockerのコンテナはOSレベルでの仮想化を行ってそのOS上でプロセスを動かすのですが、仮想マシンはマシンレベルで仮想化を行います。なので、以下のような特徴があります。

|  | コンテナ(Docker) | 仮想マシン |
|:-:|:-:|:-:|
| 起動 | 早い  |  遅い |
| リソース消費 | 少ない | 多い |
| OS | Linuxのみ | 何でも可 |

vagrantはマシンレベルで構築するので、ベースとなるOSは特に限定されませんが、dockerはLinux上でのみしか動きません。しかし、dockerはLinuxの機能を借りて仮想環境を実現しているので必要なリソースは少なくて起動が早いのが特徴です。逆に仮想マシンはOS単位で仮想化しているのでリソース消費量が高く起動は遅いです。

以前はVirtualBoxが必要だったり環境設定が必要だったり、色々面倒だったそうですが、今はDocker社が公式で出している <i class="fa fa-external-link-square"></i> [Docker for (Mac/Windows/Linux)](https://www.docker.com/products/docker)をインストールするとすぐに使えるので、まだインストールをしていない人は入れておきましょう。

### <i class="fa fa-cube"></i> `ｄocker-compose`とは?
dockerをインストールするとdockerコマンドが使用できて、それを用いることで色々なコンテナを立ち上げることができるのですが、Railsアプリの環境を構築するのにPostgresだったり、Rubyだったり、Redisだったり複数のコンテナからなるサービスを手作業で作るのは大変。ということで、docker-composeというツールを用いることにします。

docker-composeとは複数のコンテナからなるdockerアプリケーションを定義・実行るツールで、compose用の設定ファイルを用意してコマンドを実行することで、記述されたコンテナを一気に起動できます <i class="fa fa-thumbs-o-up"></i>

docker-composeはリリースページにアクセスして最新のバージョンを`curl`コマンドを使ってインストールしてください。インストールの方法はリリースページに書かれているので、コンソールにコピペすれば大丈夫だと思います。

[Docker compose release page](https://github.com/docker/compose/releases)

### <i class="fa fa-file-code-o"></i> `Dockerfile` / `docker-compose.yml`

dockerコマンドでイメージの作成やコンテナの起動などを実行してもいいのですが、コンソールから入力するのは疲れるし、作業を一括して行いたくなってくるのでそんな時に使うのが`Dockerfile`です。`makefile`のようなものですが、個人的にはそれ以上に取っ付きやすいくて気に入りました。

使えるコマンドも10種類程度しかないので、すぐなれると思います。
[Dockerfileのコマンド](http://qiita.com/kmats@github/items/27c09f34aa4d338d40fc)

`docker-compose.yml`はdocker-composeというツールを使うための設定ファイルで、ここにアプリケーションを構成する各サービスを定義します。

### <i class="fa fa-wrench"></i> 検証環境

検証はmac上でdockerは1.12, docker-composeは1.8.1を使用してdockerの環境を構築しています。2016年12月現時点の最新バージョンだと思います。

```
$ docker version
Client:
 Version:      1.12.3
 API version:  1.24
 Go version:   go1.6.3
 Git commit:   6b644ec
 Built:        Wed Oct 26 23:26:11 2016
 OS/Arch:      darwin/amd64

Server:
 Version:      1.12.3
 API version:  1.24
 Go version:   go1.6.3
 Git commit:   6b644ec
 Built:        Wed Oct 26 23:26:11 2016
 OS/Arch:      linux/amd64

$ docker-compose version
docker-compose version 1.8.1, build 878cff1
docker-py version: 1.10.3
CPython version: 2.7.9
OpenSSL version: OpenSSL 1.0.2h  3 May 2016
```

### <i class="fa fa-cube"></i> Dockerを活かす原則
#### 1) 1 Container 1 Service
１つのコンテナには1つのサービスを動作させるようにしましょう。たとえば、今回用いるPostgresとRedisを同じコンテナで動かすことも可能ですが、コンテナが複雑化してしまうので、極力シンプルに保つため意識しましょう。

#### 2) Dockerfileに書く
コンソールでdockerコマンドでコンテナを作成することも可能ですが、結局誰かと共有するにはその作成手順を明示しなくてはいけないので、Dockerfileに統一しましょう。

## <i class="fa fa-cubes"></i> Railsアプリの開発環境を構築してみる

今回の設定として、開発中のRailsアプリ(Postgres+Redis)をDockerの仮想環境に移行することを考えます。ここで個人的な前提として、開発環境ではsqlite3を使っていたのでそこでの変更手順もまとめてあります。

- ruby: 2.3.3
- Ruby on Rails: 5.0.1.rc2

### <i class="fa fa-file-code-o"></i>  Rails用のDockerfileを用意する
Dockerfileはrailsアプリのルートディレクトリに配置してください。

```bash:Dockerfile
FROM ruby:2.3.3

ENV APP_ROOT /my_app

RUN apt-get update -qq && apt-get install -y nodejs build-essential libpq-dev postgresql-client

RUN mkdir $APP_ROOT
WORKDIR $APP_ROOT

ADD Gemfile ${APP_ROOT}/Gemfile
ADD Gemfile.lock ${APP_ROOT}/Gemfile.lock
RUN bundle install

ADD . $APP_ROOT
```

Dockerfileの最初は`FROM IMAGE:TAG`コマンドで、使用するベースイメージを選択しなくてはいけません。使用するイメージは`CentOS`だったり、り`Redis`だったり他人の作成したイメージを指定できますが基本的には、[公式のイメージリスト](https://hub.docker.com/explore/)から選択すればいいでしょう。

`Ubuntu`を入れて、`apt-get`で1からrubyの環境を構築することも出来ますが、今回はあらかじめrubyの入っているイメージを選択しました。

`ENV`コマンドは環境変数を設定するコマンドで`APP_ROOT`という変数に`/my_app`という文字列を格納しています。

`RUN`コマンドはを今回だとrubyの入ったコンテナ内のコンソールで実行する感じで、`apt-get`で`nodejs`などをインストールしています。

コンテナ内に、作業用にディレクトリを作ってそこを`WORKDIR`コマンドで起点とします。その作業ディレクトリに`ADD`コマンドで自分のローカルの`Gemfile`と`Gemfile.lock`をコピーして、コンテナ内で`bundle install`を実行した後、他のローカルのファイルすべてをコンテナ内にコピーしています。

#### <i class="fa fa-smile-o"></i> メモ
他のサイトでは`Gemfile`を`/tmp`フォルダにコピーして`bundle install`をすることで高速化している例がありますが、Docker 1.1以上からは`ADD`コマンドにキャッシュ機能が増えたので、変更がなければ、再ビルドをしても高速で行えます。

同様の理由で、`ADD . $APP_ROOT`コマンドで、ローカルの作業ディレクトリを全てコンテナ内にコピーをしているのですが、ここでログファイル等もコピーしてしまうと、変更を加えていないのに再ビルドが走る可能性があるのでコピー対象から明示的に外しておきましょう。これは`.dockerignore`で実現できます。

```bash:.dockerignore
## Rails
public/assets/*
.ruby-version
.env

## Environment normalisation:
.bundle
vendor/bundle
.git*

## log
log/*
tmp/*

## OSX
.DS_Store

## docker
.dockerignore
```

### <i class="fa fa-file-code-o"></i>  docker-composeでオーケストレーション
ちなみに...
> オーケストレーションとは、複雑なコンピュータシステム/ミドルウェア/サービスの配備/設定/管理の自動化を指す用語

お恥ずかしいですが、意味が分からなかったのでググりました。docker-composeで色々なサービス(Postgres, Redis, Nginx, etc...)などを一括で設定・起動するので、しっくりきますね。

```bash:docker-compose.yml
version: '2'
services:
  db:
    image: postgres
    ports:
      - "5432"
  redis:
    image: redis:latest
    ports:
      - 6379:6379
  web:
    build: .
    command: bundle exec rails s -p 3000 -b 0.0.0.0
    ports:
      - "3000:3000"
    links:
      - db
      - redis
    environment:
      DATABASE_USER: postgres
      DATABASE_PASSWORD:
      DATABASE_PORT: 5432
      DATABASE_HOST: db
      REDIS_URL: redis://redis:6379
```

`version '2'`や、はじめの`services`はDocker1.5系以降のバージョンで必要なおまじないだと思ってください。`services`の下の階層の`db`, `redis`, `web`はそれぞれサービス名を意味していて、この名前のコンテナが立ち上がって、それ以下の内容がそのコンテナ内で実現される感じです。

RedisコンテナとDBコンテナは、`redis`と`postgres`は公式のイメージが用意されているので、それをそのまま利用しています。

RailsコンテナはDockerfileで先程準備したコンテナを`build: .`で指定して、~~`links`でDBコンテナとRedisコンテナを同時に起動するように設定しています。~~`environment`は、そのコンテナ内で使用できる環境変数を定義していてます。

> docker-composeで定義したserviceは同一のネットワーク配下に置かれるので、`links`で定義しなくても、お互いのホスト名として利用できるので、記述しなくても大丈夫なそうです。

@hidekuro さんにコメントで訂正していただきました。ありがとうございます。

#### <i class="fa fa-smile-o"></i> メモ
`image: postgres`と公式のpostgresイメージを使用していますが、正式な書き方は`イメージ名:タグ名`です。このようにタグ名を省略した場合は、`postgres:latest`と同じとみなされて最新のバージョンが適用されます。なので、バージョン指定が必要な場合は、以下から見つけるといいでしょう。

https://hub.docker.com/_/postgres/

`ports`はDocker内外にポートを開放する設定でRailsコンテナはブラウザからアクセスしたりするので、`ports`でポートを開放する必要がありますが、`Redis`コンテナや`DB`コンテナは`Rails`コンテナからしかアクセスしないなら、`expose`でDocker内の他のコンテナに向けてポートを開放すべきかもしれないです。

`postgres`と`redis`の公式イメージは`ports`を指定しなくても、デフォルトでそれぞれ`5432`と`6379`を指しますが、明示しておきます。

### <i class="fa fa-file-code-o"></i> RailsのDBの設定

今回は前提として、開発環境ではsqlite3を使用していたので、まずそれをpostgresに変更します。その次に、RailsコンテナとDockerで用意したDBコンテナは別のコンテナなので、`database.yml`を書き換えて接続の設定を行います。

#### sqlite3 <i class="fa fa-long-arrow-right"></i> postgres
`Gemfile`から`sqlite3`を削除して、本番環境のみに記述していた`pg`を上に持ってきました。

```ruby:Gemfile
+ gem 'pg'

group :development, :test do
-  gem 'sqlite3'
end

group :production do
-  gem 'pg'
end
```

#### <i class="fa fa-file-code-o"></i> `database.yml`

環境変数を多様していますが、この中身は先ほど`docker-compose.yml`で定義しているので、改めて確認します。

```
DATABASE_USER: postgres
DATABASE_PASSWORD:
DATABASE_PORT: 5432
DATABASE_HOST: db
```

HOST名やUSER, PASSWORDはこれじゃないといけないの？と思った方は下記のメモ欄に書いているので参考にしてください。自分も結局他の方法はよくわからなかったのでデフォルトの設定で行きました...

```yaml:database.yml
default: &default
  adapter: postgresql
  encoding: unicode
  timeout: 5000
  port: <%= ENV.fetch('DATABASE_PORT') { 5432 } %>
  host: <%= ENV.fetch('DATABASE_HOST') { 'localhost' } %>
  username: <%= ENV.fetch('DATABASE_USER') { 'root' } %>
  password: <%= ENV.fetch('DATABASE_PASSWORD') { 'password' } %>
  pool: <%= ENV.fetch('RAILS_MAX_THREADS') { 5 } %>

development:
  <<: *default
  database: myapp_development

test:
  <<: *default
  database: myapp_test
```


#### <i class="fa fa-code-o"></i> `redis`の設定

`database.yml`で環境変数に`REDIS_URL: redis://redis:6379`と設定しました。なので、redisの初期設定を行っているファイルを以下のように変更しました。

```ruby:config/initialize/redis.rb
REDIS ||= Redis.new(url: ENV['REDIS_URL'] || 'redis://localhost:6379')
```

ここは適宜、自分の環境に合わせて変更してください。

#### <i class="fa fa-smile-o"></i> メモ
今回は公式の`Postgres`を使用しましたが、[本家ドキュメントの日本語訳](http://docs.docker.jp/compose/rails.html#id3)によると

>デフォルトでは、データベースは localhost で実行するとみなされます。そのため、db コンテナに指示しなくてはいけません。postgres イメージにデフォルトで設定されている database と username に変更する必要があります。

と書かれています。これに気付かずかなりハマりました。そのデフォルトのdatabaseとusernameは

```
development: &default
  adapter: postgresql
  encoding: unicode
  database: postgres
  pool: 5
  username: postgres
  password:
  host: db
```

となっているので、これを参考に　database.ymlを変更しました。

また、上記でREDISに接続するために、`redis://redis:6379`でRedisコンテナに、`database.yml`では`host: db`でDBコンテナにRailsコンテナから接続していますが、これにもDockerのおかげです。
`docker-compose.yml`でlinksを書くと、コンテナ内の`/etc/hosts`に指定したサービス名とそのipアドレスが追加されます。以下は例です。

```bash:/etc/hosts
172.17.0.6  db
172/17/0.4  redis
```

このように、今回指定したサービス名がエイリアスとなって、ipアドレスを参照できるようになるので、`host: db`とコンテナ名を入力したり、`redis://redis:6379`で他のコンテナにアクセス出来ました。わざわざipアドレスとか気にしなくていいのは楽ですね！！

## <i class="fa fa-cubes"></i> 実行してみよう!
これで設定は完了です。

```bash
$ docker-compose build
... # これで設定したpostgresやrubyやら色々なイメージとかを取ってくる
... # 何もエラーが無ければ、環境構築完了なので、RailsアプリのDB設定を行います
$ docker-compose run web rake db:create
$ docker-compose run web rake db:migrate
... # docker-compose run <サービス名> <コマンド>
... # 上の構文でサービス名のコンテナ内でコマンドを実行できる
$ docker-compose up
```

これで全てのコンテナが起動して、以下のようなログが出力されると思います。

```
web_1    | => Booting Puma
web_1    | => Rails 5.0.1.rc2 application starting in development on http://0.0.0.0:3000
web_1    | => Run `rails server -h` for more startup options
web_1    | Puma starting in single mode...
web_1    | * Version 3.6.2 (ruby 2.3.3-p222), codename: Sleepy Sunday Serenity
web_1    | * Min threads: 5, max threads: 5
web_1    | * Environment: development
web_1    | * Listening on tcp://0.0.0.0:3000
web_1    | Use Ctrl-C to stop
```

ログの通り、`http://0.0.0.0:3000`にアクセスすれば、ちゃんと動いているのが確認できました！！これにて完了...ではないんです実は(´・ω・｀)すいません。

### このままだとbuildするたびにデータが消えてしまう!!

このままだと、RedisやPostgresのデータベースを再起動するたびに中に入っていたデータが消えてしまいます。なので、Dockerの標準のAPIを用いてvolumesで設定を行います。今回はpostgresとredisのデータを永続化してほしいので、その設定を行います。

```yaml:docker-compose.yml
version: '2'
services:
  db:
    image: postgres
    ports:
      - "5432"
    volumes:
      - ./data/postgres:/var/lib/postgresql/data
  redis:
    image: redis:latest
    ports:
      - 6379:6379
    volumes:
      - ./data/redis:/data
    command: redis-server --appendonly yes
```

DBコンテナとRedisコンテナにvolumesとRedisコンテナにはcommandを付与しました。volumesはオプションとしてホスト側のパスとコンテナ側のパスを指定して、ホスト側のそのパスにコンテナ側のパスをマウントします。こうすることで、コンテナが再起動した時に、自分のローカルの方にも保存されたデータがコンテナ側にも同期されて、それを読み込んだ状態からスタートする...みたいな感じになるので、データを永続化できます。

`volumes: <ホストのパス>:<コンテナのパス>`

`redis`は初期起動時にコマンドを変更して`--appendonly yes`というオプションをつけています。これをつけないとデータが生成されないらしいので、気をつけてください。
`redis`は`/data`に、`postgres`は`/var/lib/postgresql/data`にデータをマウントすればいいので、以上のように編集すれば、完了です！

## <i class="fa fa-bug"></i> ハマった事一覧

### `A server is already running`
Dockerの環境が出来て,`docker-compose build`も出来て、いざ`docker-compose up`じゃあ～！ってなったら顔を出したエラー。

```
web_1    | => Booting Puma
web_1    | => Rails 5.0.1.rc2 application starting in development on http://0.0.0.0:3000
web_1    | => Run `rails server -h` for more startup options
web_1    | A server is already running. Check /myapp/tmp/pids/server.pid.
web_1    | Exiting
```

すでにサーバーが立っているよという。でも、`pa aux | grep 3000`で探してみても、`docker ps -a | grep 3000`で探してみても別にプロセスは立っていなさそう。。
これの原因はRailsのWEBサーバが正常に終了出来ていないことによるそうで、本来ならpidはWEBサーバを起動する時に`tmp/pids/server.pid`に書き込まれて、終了する時に削除されるはずだけれど、そのファイルの中に何か書かれているがために、WEBサーバが起動中であると誤認されていることによるエラーのよう。

`rm /tmp/pids/server.pid`でファイルを消せばサーバを起動できました。`server.pid`はなければ、生成されるので消しても大丈夫です。

### `Bundler::GemRequireError: There was an error while trying to load the gem 'uglifier'.`

Dockerfileを実行時の`bundle install`でこんなエラーが発生しました。ググったら、node.jsが入っていれば大丈夫って書いてあったので、`Dockerfile`に`apt-get install nodejs`を追記したら直りました。これは上では対応済みです！

## <i class="fa fa-support"></i> dockerのTips

### エラーが起きたらログを見よう

`docker-compose logs`で実行中のコンテナ内のログが見れます。サービス名を指定したい場合は、`docker-compose logs db`とすれば、DBコンテナ内のログが見れます。

### 不要なプロセスとイメージを削除する
いま起動中のコンテナは`docker images`コマンドで、実行中のプロセスは`docker ps`コマンドで分かります。しかし、起動していなかったり、すでに必要のないプロセスを削除しないと以外と容量を食ってしまうので、定期的に不要なプロセスやコンテナを削除するといいでしょう。

ステータスがexitedのプロセスを削除して、イメージ名が<none>となっているコンテナを削除するワンライナーです。

`docker ps -aq -f "status=exited" | xargs docker rm -v && docker images -q -f "dangling=true" | xargs docker rmi`

### `docker-compose` コマンドを短くする
基本的に今まで普通に実行していた`rake db:migrate`などのコマンドは今回構成したDockerコンテナ上だと`docker-compose run web rake db:migrate`というようにかなり長くなってしまいます。なので、.zshrcや.bashrcに以下のように記述してエイリアス化しましょう。

```
alias fig='docker-compose'
```

## <i class="fa fa-cubes"></i> さいごに

かなり長文になってしまい申し訳ありません...。自分なりに後から読み返しても納得できるように詳しく書いたつもりなので、もし間違っていたら訂正リクエスト等していただけると嬉しいです！

dockerの[公式ドキュメント](https://docs.docker.com/)はかなり充実しているので、迷ったらまずそこを見るのがいいと思います。といって、全て英語なので、もしいきなりは厳しいという方がいましたら、[日本語訳のドキュメント](http://docs.docker.jp/)もあるのでぜひ一度は読んでみてください！

あと、書いていて完全にOthloTechのAdventCalendarということを忘れていましたが、[OthloTech](http://www.othlo.tech)は来年も東海のIT界隈を盛り上げるために楽しく活動していきますので、応援していただけると嬉しいです＼(^o^)／


# (」ﾟロﾟ)」<<< { A Happy New Year ♬♬ ♪}

![hny.jpg](https://qiita-image-store.s3.amazonaws.com/0/85665/99d1a390-00c1-e02f-e4c3-7c6af6c4d3eb.jpeg)


## <i class="fa fa-book"></i> 参考リンク
- [2016年版、Dockerのすべてが5分でわかるまとめ!(コマンド一覧付き)](http://paiza.hatenablog.com/entry/docker_intro)
- [docker-compose で Rails の開発環境を作る
](http://qiita.com/skyriser/items/2cf98b747ed6577cc5ee)
- [Docker ComposeによるMySQL, Redis, nginx, Rails構成の設定例](http://qiita.com/oshin/items/0d011610238eb4df83fd)
- [docker-compose-rails-dev-example](https://github.com/vovimayhem/docker-compose-rails-dev-example)
- [docker-composeでデータベースコンテナを立てるときのTips](http://blog.muuny-blue.info/9f067d8d6df2d4b8c64fb4c084d6c208.html)
- [Docker Compose - docker-compose.yml リファレンス](http://qiita.com/zembutsu/items/9e9d80e05e36e882caaa)
- [複雑な構成のrailsアプリをdockerで動かしたい場合はどうするべきか](http://qiita.com/k-shogo/items/3c887cbf86b3cab85b72)